lib/ex_sni.ex

defmodule ExSni do
  use Supervisor

  alias ExSni.Bus
  alias ExSni.{Icon, Menu}

  def start_link(init_opts \\ [], start_opts \\ []) do
    Supervisor.start_link(__MODULE__, init_opts, start_opts)
  end

  @impl true
  def init(opts) do
    with {:ok, service_name} <- get_optional_name(opts),
         {:ok, icon} <- get_optional_icon(opts),
         {:ok, menu} <- get_optional_menu(opts) do
      menu_server_pid = {:via, Registry, {ExSniRegistry, "menu_server"}}
      dbus_service_pid = {:via, Registry, {ExSniRegistry, "dbus_service"}}
      backup_store_pid = {:via, Registry, {ExSniRegistry, "menu_store"}}

      router = %ExSni.Router{
        icon: icon,
        menu: menu_server_pid
      }

      children = [
        {Registry, keys: :unique, name: ExSniRegistry},
        %{
          id: ExDBus.Service,
          start:
            {ExDBus.Service, :start_link,
             [
               [name: service_name, schema: ExSni.Schema, router: router, cookie: :system_user],
               [name: dbus_service_pid]
             ]},
          restart: :transient
        },
        %{
          id: Menu.BackupStore,
          start: {Menu.BackupStore, :start_link, [[], [name: backup_store_pid]]},
          restart: :transient
        },
        # Menu versions server
        %{
          id: Menu.Server,
          start:
            {Menu.Server, :start_link,
             [
               [
                 menu: menu,
                 dbus_service: dbus_service_pid,
                 backup_server: backup_store_pid
               ],
               [name: menu_server_pid]
             ]},
          restart: :transient
        },
        {Task.Supervisor, name: ExSni.Task.Supervisor}
      ]

      Supervisor.init(children, strategy: :rest_for_one)
    end
  end

  @doc """
  Returns true if there is a StatusNotifierWatcher available
  on the Session Bus.
  """
  @spec is_supported?() :: boolean()
  def is_supported?() do
    Bus.is_supported?()
  end

  @doc """
  Returns true if there is a StatusNotifierWatcher available
  on the Session Bus.
  - sni_pid - The pid of the ExSni Supervisor
  """
  @spec is_supported?(GenServer.server()) :: boolean()
  def is_supported?(sni_pid) do
    case get_bus(sni_pid) do
      nil -> false
      bus_pid when is_pid(bus_pid) -> Bus.is_supported?(bus_pid)
      _ -> false
    end
  end

  @doc """
  Returns {:ok, sni_pid} if there is a StatusNotifierWatcher available
  on the Session Bus. Returns {:error, reason} otherwise.
  - sni_pid - The pid of the ExSni Supervisor
  """
  @spec get_supported(GenServer.server()) ::
          {:ok, bus_pid :: GenServer.server()} | {:error, reason :: binary()}
  def get_supported(sni_pid) do
    case get_bus(sni_pid) do
      nil ->
        {:error, "Service has no DBUS connection"}

      bus_pid when is_pid(bus_pid) ->
        if Bus.is_supported?(bus_pid) do
          {:ok, bus_pid}
        else
          {:error, "Unable to connect to StatusNotifierWatcher over D-Bus"}
        end

      error ->
        error
    end
  end

  @spec close(GenServer.server()) :: :ok
  def close(sni_pid) do
    Supervisor.stop(sni_pid)
  end

  @spec get_menu(
          GenServer.server()
          | {:router, ExSni.Router.t()}
          | {:server, GenServer.server() | atom() | tuple()}
        ) ::
          {:ok, nil | Menu.t()} | {:error, any()}
  def get_menu(sni_pid) when is_pid(sni_pid) or is_atom(sni_pid) do
    case get_menu_handle(sni_pid) do
      {:ok, handle} ->
        Menu.Server.get(handle)

      error ->
        error
    end
  end

  @spec set_menu(GenServer.server(), Menu.t() | nil) :: {:ok, Menu.t() | nil} | {:error, any()}
  def set_menu(sni_pid, menu) do
    case get_menu_handle(sni_pid) do
      {:ok, handle} -> Menu.Server.set(handle, menu)
      error -> error
    end
  end

  @spec update_menu(
          sni_pid :: GenServer.server(),
          parentId :: nil | integer(),
          menu :: nil | Menu.t()
        ) :: any()
  def update_menu(sni_pid, nil, menu) do
    set_menu(sni_pid, menu)
  end

  @spec get_icon(GenServer.server()) :: {:ok, nil | Icon.t()} | {:error, any()}
  def get_icon(sni_pid) do
    case get_router(sni_pid) do
      {:ok, %ExSni.Router{icon: icon}} -> {:ok, icon}
      error -> error
    end
  end

  @spec set_icon(pid, Icon.t() | nil, keyword()) :: {:ok, Icon.t() | nil} | {:error, any()}
  def set_icon(sni_pid, icon, opts \\ []) do
    with {:ok, service_pid} <- get_service_pid(sni_pid),
         {:ok, router} <- get_service_router(service_pid),
         {:ok, %{icon: icon} = router} <- set_service_router(service_pid, %{router | icon: icon}) do
      if Keyword.get(opts, :register, true) == true and icon != nil do
        register_router_icon(service_pid, router)
      else
        {:ok, icon}
      end
    end
  end

  @spec update_icon(sni_pid :: GenServer.server(), icon :: nil | Icon.t()) :: any()
  def update_icon(sni_pid, icon) do
    with {:ok, icon} <- set_icon(sni_pid, icon) do
      send_icon_signal(sni_pid, "NewIcon")
      {:ok, icon}
    end
  end

  @spec register_icon(pid) :: :ok | {:error, any()}
  def register_icon(sni_pid) do
    with {:ok, service_pid} <- get_service_pid(sni_pid),
         {:ok, router} <- get_service_router(service_pid),
         {:ok, _} <- register_router_icon(service_pid, router) do
      :ok
    end
  end

  defp register_router_icon(_, %{icon: %Icon{} = icon, icon_registered: true}) do
    {:ok, icon}
  end

  defp register_router_icon(service_pid, %{icon: %Icon{}, icon_registered: false} = router) do
    with {:ok, _} <- register_icon_on_service(service_pid),
         {:ok, %{icon: icon}} <-
           set_service_router(service_pid, %{router | icon_registered: true}) do
      {:ok, icon}
    end
  end

  defp register_router_icon(_, _) do
    {:error, "Cannot register nil icon"}
  end

  defp register_icon_on_service(service_pid) do
    case ExDBus.Service.get_name(service_pid) do
      nil -> service_register_icon(service_pid, nil)
      name when is_binary(name) -> service_register_icon(service_pid, name)
    end
  end

  defp get_bus(sni_pid) do
    with {:ok, service_pid} <- get_service_pid(sni_pid) do
      ExDBus.Service.get_bus(service_pid)
    end
  end

  defp get_menu_handle(sni_pid) do
    case get_router(sni_pid) do
      {:ok, %ExSni.Router{menu: {:via, _, _} = server_via}} ->
        {:ok, server_via}

      {:ok, %ExSni.Router{menu: server_pid}} when is_pid(server_pid) ->
        {:ok, server_pid}

      error ->
        error
    end
  end

  defp get_router(sni_pid) do
    with {:ok, service_pid} <- get_service_pid(sni_pid) do
      get_service_router(service_pid)
    end
  end

  defp get_service_router(service_pid) do
    case ExDBus.Service.get_router(service_pid) do
      {:ok, nil} -> {:ok, %ExSni.Router{}}
      ret -> ret
    end
  end

  defp set_service_router(service_pid, router) do
    ExDBus.Service.set_router(service_pid, router)
  end

  defp service_register_icon(service_pid, nil) do
    with {:ok, dbus_pid} <- ExDBus.Service.get_dbus_pid(service_pid) do
      service_register_icon(service_pid, dbus_pid)
    end
  end

  defp service_register_icon(service_pid, service_name) do
    GenServer.call(service_pid, {
      :call_method,
      "org.kde.StatusNotifierWatcher",
      "/StatusNotifierWatcher",
      "org.kde.StatusNotifierWatcher",
      "RegisterStatusNotifierItem",
      {"s", [:string], [service_name]}
    })
  end

  defp get_service_pid(_sni_pid) do
    {:ok, {:via, Registry, {ExSniRegistry, "dbus_service"}}}
  end

  defp send_icon_signal(sni_pid, signal)
       when signal in ["NewTitle", "NewIcon", "NewAttentionIcon", "NewOverlayIcon", "NewToolTip"] do
    send_signal(sni_pid, :icon, signal, {"", [], []})
  end

  defp send_signal(sni_pid, :icon, signal, args) do
    with {:ok, service_pid} <- get_service_pid(sni_pid) do
      ExDBus.Service.send_signal(
        service_pid,
        "/StatusNotifierItem",
        "org.kde.StatusNotifierItem",
        signal,
        args
      )
    end
  end

  defp get_optional_name(opts) when is_list(opts) do
    version = Keyword.get(opts, :version, 1)

    case Keyword.get(opts, :name, nil) do
      nil -> {:ok, nil}
      "" -> {:ok, nil}
      name when is_binary(name) -> {:ok, "#{name}-#{:os.getpid()}-#{version}"}
      _ -> {:stop, "Given DBus name is not a valid string"}
    end
  end

  defp get_optional_name(_) do
    {:ok, nil}
  end

  defp get_optional_icon(opts) when is_list(opts) do
    case Keyword.get(opts, :icon, nil) do
      nil -> {:ok, nil}
      %Icon{} = icon -> {:ok, icon}
      _ -> {:stop, "Invalid \"icon\" option. Not a Icon struct"}
    end
  end

  defp get_optional_icon(_) do
    {:ok, nil}
  end

  defp get_optional_menu(opts) when is_list(opts) do
    case Keyword.get(opts, :menu, nil) do
      nil -> {:ok, nil}
      %Menu{} = menu -> {:ok, menu}
      _ -> {:stop, "Invalid \"menu\" option. Not a Menu struct"}
    end
  end

  defp get_optional_menu(_) do
    {:ok, nil}
  end
end