Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- `Desktop.Platform.Content.reload/1` and `Desktop.Window.reload/1` — backend-safe webview reload (replaces `:wxWebView.reload/1`)
- Test suite: `mix test.fast`, `xvfb-run mix test.wx`, `mix test.guard` — see `docs/TEST_PLAN.md`
- Compile without OTP `:wx`: conditional `erl_src_paths` and `Desktop.Wx` fallbacks (no `wx.hrl` required)
- `Desktop.Env` subscriber path/URL payloads (`:open_url`, `:open_file`, `:print_file`) are canonical UTF-8 binaries; wx charlists are normalized at the Env boundary before buffer or delivery

## Changes in 1.5

Expand All @@ -28,7 +29,7 @@

## Changes in 1.3

- Added `Env.subscribe/1` to listen to MacOS application events such as `{:open_url, url}` when a url associated with the application is clicked.
- Added `Env.subscribe/0` to listen to MacOS application events such as `{:open_url, [url]}` when a url associated with the application is clicked.
- Using (experimental) dbus support to render the systray icon on linux
- Added `Menu.escape_attribute/1`
- Added `Window.url/1`
Expand Down
78 changes: 66 additions & 12 deletions lib/desktop/env.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,43 @@ defmodule Desktop.Env do
Also it has a global connect() method to allow binding of :wx event callbacks using
this long lived process as reference.

Subscribers (see `subscribe/0`) also receive:
## Subscriber events

Processes that call `subscribe/0` receive `t:subscriber_event/0` messages.

Path and URL payloads are **canonical UTF-8 binaries** (`String.t()`). Wx and
other sources may pass charlists; `Desktop.Env` normalizes them at the boundary
(both on `notify_subscribers/1` and on OS `handle_info` ingest) before buffering
or delivering to subscribers.

* `{:print_file, [path]}` / `{:open_file, [path]}` / `{:open_url, [url]}` —
zero or more path/URL binaries
* `{:new_file, []}`
* `{:desktop, :window_activated, window_id}` — a `Desktop.Window` with registered
`id` has become the active frame (user brought the app window to the foreground).
`id` atom has become the active frame
"""
alias Desktop.Env
use GenServer
require Logger

@typedoc "UTF-8 path or URL string delivered to Env subscribers."
@type path_or_url :: String.t()

@typedoc "OS application open/print events (paths and URLs are binaries)."
@type os_app_event ::
{:print_file, [path_or_url()]}
| {:open_file, [path_or_url()]}
| {:open_url, [path_or_url()]}
| {:new_file, []}

@typedoc "Desktop lifecycle events."
@type desktop_event :: {:desktop, :window_activated, atom()}

@typedoc "Messages delivered to processes that called `subscribe/0`."
@type subscriber_event :: os_app_event() | desktop_event()

@path_tags [:open_url, :open_file, :print_file]

defstruct [:wx_env, :wx, :map, :waiters, :windows, :sni, :events, :subs]

@doc false
Expand Down Expand Up @@ -123,6 +151,8 @@ defmodule Desktop.Env do
end

def handle_cast({:notify_subscribers, message}, state = %Env{subs: subs}) do
message = normalize_subscriber_event(message)

for sub <- subs do
send(sub, message)
end
Expand Down Expand Up @@ -158,6 +188,8 @@ defmodule Desktop.Env do

def handle_info({_mac_event, list} = e, state = %Env{subs: subs, events: events})
when is_list(list) do
e = normalize_subscriber_event(e)

if subs == [] do
{:noreply, %Env{state | events: events ++ [e]}}
else
Expand Down Expand Up @@ -266,30 +298,52 @@ defmodule Desktop.Env do
end

@doc """
Wrapper around wx.subscribe()
Subscribe the calling process to OS and desktop lifecycle events.

Will send to the calling process events in the form:
Delivers `t:subscriber_event/0` messages. Path and URL lists are always
UTF-8 binaries (never charlists), including when the OS or bridge originally
supplied charlists.

* `{:print_file, [filename]}`
* `{:open_file, [filename]}`
* `{:open_url, [filename]}`
* `{:new_file, []}`
* `{:desktop, :window_activated, window_id}` — from `Desktop.Window` when the
frame becomes active (see `Desktop.Env` module doc).
* `{:print_file, [path_or_url]}`
* `{:open_file, [path_or_url]}`
* `{:open_url, [path_or_url]}`
* `{:new_file, []}`
* `{:desktop, :window_activated, window_id}` — `window_id` is the window `id` atom
"""
def subscribe() do
GenServer.call(__MODULE__, {:subscribe, self()})
end

@doc """
Delivers a message to all processes that called `subscribe/0`.
Delivers a `t:subscriber_event/0` to all processes that called `subscribe/0`.

Used internally by `Desktop.Window` for lifecycle events (e.g. frame activation).
Used by `Desktop.Window` for lifecycle events and by native bridges (e.g.
EventBridge) for OS open-URL/file notifications. Callers may pass charlist
paths/URLs; they are normalized to binaries before delivery.
"""
def notify_subscribers(message) when is_tuple(message) do
GenServer.cast(__MODULE__, {:notify_subscribers, message})
end

defp normalize_subscriber_event({tag, paths})
when tag in @path_tags and is_list(paths) do
{tag, normalize_paths(paths)}
end

defp normalize_subscriber_event(other), do: other

# Bare wx charlist as the entire second element: {:open_url, ~c"https://..."}.
defp normalize_paths([c | _] = charlist) when is_integer(c) do
[List.to_string(charlist)]
end

defp normalize_paths(paths) when is_list(paths) do
Enum.map(paths, &normalize_path/1)
end

defp normalize_path(bin) when is_binary(bin), do: bin
defp normalize_path(list) when is_list(list), do: List.to_string(list)

defp init_sni() do
{task, ref} = spawn_monitor(fn -> exit(do_init_sni()) end)

Expand Down
76 changes: 60 additions & 16 deletions test/desktop/env_test.exs
Original file line number Diff line number Diff line change
@@ -1,33 +1,77 @@
defmodule Desktop.EnvTest do
use ExUnit.Case, async: false
use Desktop.Test.DesktopCase, async: false

describe "browser backend" do
test "T-ENV-03: wx_use_env with nil wx_env is no-op" do
with_backend(:browser, fn ->
restart_desktop!()
assert :ok = Desktop.Env.wx_use_env()
end)
end
end

describe "subscriber payload normalization" do
setup do
previous = Application.get_env(:desktop, :backend, :auto)
Application.put_env(:desktop, :backend, :browser)
restart_desktop!()
:ok = Desktop.Env.subscribe()

try do
{:ok, _} = Application.ensure_all_started(:desktop)
assert :ok = Desktop.Env.wx_use_env()
after
on_exit(fn ->
Application.put_env(:desktop, :backend, previous)
end
end)

:ok
end

test "notify_subscribers keeps binary open_url paths" do
Desktop.Env.notify_subscribers({:open_url, ["ddrive://x"]})
assert_receive {:open_url, [url]}, 1000
assert url == "ddrive://x"
assert is_binary(url)
end

test "notify_subscribers converts charlist open_url paths to binaries" do
Desktop.Env.notify_subscribers({:open_url, [~c"ddrive://x"]})
assert_receive {:open_url, ["ddrive://x"]}, 1000
end

test "notify_subscribers converts list of charlist open_file paths" do
Desktop.Env.notify_subscribers({:open_file, [~c"/tmp/a", ~c"/tmp/b"]})
assert_receive {:open_file, ["/tmp/a", "/tmp/b"]}, 1000
end

test "wx ingest with bare charlist second element becomes binary list" do
send(Desktop.Env, {:open_url, ~c"ddrive://bare"})
assert_receive {:open_url, ["ddrive://bare"]}, 1000
end
end

describe "wx backend (T-ENV-04)" do
use Desktop.Test.WxCase
test "buffered OS events before any subscriber are normalized binaries" do
Application.put_env(:desktop, :backend, :browser)
restart_desktop!()

# No subscribers yet — event is buffered and normalized on ingest.
send(Desktop.Env, {:open_url, [~c"ddrive://buffered"]})
Process.sleep(50)

test "Env stores wx and wx_env after init" do
wx = Desktop.Env.wx()
env = Desktop.Env.wx_env()
:ok = Desktop.Env.subscribe()
assert_receive {:open_url, ["ddrive://buffered"]}, 1000
end

assert wx != nil
assert env != nil
test "notify_subscribers leaves window_activated unchanged" do
Desktop.Env.notify_subscribers({:desktop, :window_activated, :TestWindow})
assert_receive {:desktop, :window_activated, :TestWindow}, 1000
end

test "wx_use_env succeeds" do
assert :ok = Desktop.Env.wx_use_env()
test "notify_subscribers leaves new_file unchanged" do
Desktop.Env.notify_subscribers({:new_file, []})
assert_receive {:new_file, []}, 1000
end
end

defp restart_desktop! do
_ = Application.stop(:desktop)
{:ok, _} = Application.ensure_all_started(:desktop)
:ok
end
end
15 changes: 15 additions & 0 deletions test/desktop/env_wx_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule Desktop.EnvWxTest do
use Desktop.Test.WxCase

test "T-ENV-04: Env stores wx and wx_env after init" do
wx = Desktop.Env.wx()
env = Desktop.Env.wx_env()

assert wx != nil
assert env != nil
end

test "T-ENV-04: wx_use_env succeeds" do
assert :ok = Desktop.Env.wx_use_env()
end
end
Loading