lib/desktop/os.ex

defmodule Desktop.OS do
  @moduledoc """
    The OS module provides shortcuts and helper functions
    to access OS specific information.

    Most significant one should use OS.type() to differentiate
    between the currently supported environments:
    - Android
    - IOS
    - MacOS
    - Windows
    - Linux

  """

  @doc """
    Returns the users home directory
  """
  @spec home :: binary
  def home() do
    {:ok, [[home]]} = :init.get_argument(:home)
    List.to_string(home)
  end

  @doc false
  def raise_frame(frame) do
    if type() == MacOS do
      name = System.get_env("EMU", "beam.smp")

      fn -> System.cmd("open", ["-a", name], stderr_to_stdout: true) end
      |> spawn_link()
    else
      # Calling  this on wxDirDialog segfaults on macos..
      :wxTopLevelWindow.setFocus(frame)
      :wxWindow.raise(frame)
    end
  end

  @spec type :: Linux | MacOS | Windows | Android | IOS
  def type() do
    case System.get_env("ELIXIR_DESKTOP_OS", nil) do
      "android" ->
        Android

      "ios" ->
        IOS

      _ ->
        case :os.type() do
          {:unix, :darwin} -> MacOS
          {:unix, :linux} -> Linux
          {:win32, _} -> Windows
        end
    end
  end

  def shutdown() do
    spawn(&halt/0)
  end

  @spec halt() :: no_return()
  defp halt() do
    Process.sleep(300)

    # With System.stop(0) shutdown of the WebView takes
    # a very long time (10+ seconds), and wxWidgets cleanup
    # often crashes on macOS
    # System.stop(0)

    kill_heart()
    System.halt(0)
  end

  def restart() do
    if windows?() or mobile?() do
      # on windows reinitializing wx-widgets does work sometimes...
      :init.restart()
    else
      # relying on heart
      kill_beam()
    end
  end

  def windows?() do
    type() == Windows
  end

  def mobile?() do
    case type() do
      Android -> true
      IOS -> true
      _other -> false
    end
  end

  defp kill_heart() do
    heart = Process.whereis(:heart)

    if heart != nil do
      {:links, links} = Process.info(Process.whereis(:heart), :links)
      port = Enum.find(links, fn id -> is_port(id) end)

      if port != nil do
        {:os_pid, heart_pid} = Port.info(port, :os_pid)

        kill_process(heart_pid)

        # kill thyself
        kill_beam()
      end
    end
  end

  defp kill_beam() do
    kill_process(:os.getpid())
  end

  def kill_process(pid) do
    if windows?() do
      System.cmd("taskkill", ["/f", "/pid", "#{pid}"], stderr_to_stdout: true)
    else
      System.cmd("kill", ["-9", "#{pid}"], stderr_to_stdout: true)
    end
  end

  @doc """
  This is a Path.expand variant that normalizes the drive letter
  on windows
  """
  @spec path_expand(IO.chardata()) :: binary
  def path_expand(path) do
    if windows?() do
      path = if is_list(path), do: List.to_string(path), else: path
      drv = String.downcase(String.first(path))

      if byte_size(path) == 2 and String.at(path, 1) == ":" do
        Path.expand(path <> "/")
      else
        Path.expand(path)
      end
      |> String.replace_prefix(drv <> ":", String.upcase(drv) <> ":")
    else
      Path.expand(path)
    end
  end

  @doc """
  Replacement for the :wx_misc.launchDefaultBrowser function
  """
  def launch_default_browser(file) when is_list(file) do
    List.to_string(file)
    |> launch_default_browser()
  end

  def launch_default_browser(file) do
    spawn(fn ->
      case type() do
        MacOS ->
          System.cmd("open", [file], stderr_to_stdout: true, parallelism: true)

        Linux ->
          System.cmd("xdg-open", [file], stderr_to_stdout: true, parallelism: true)

        _other ->
          Desktop.Env.wx_use_env()
          :wx_misc.launchDefaultBrowser(file)
      end
    end)
  end
end