From 2c008804fa3f84f973a4721016ff9907505defd3 Mon Sep 17 00:00:00 2001 From: Dominic Letz Date: Tue, 21 Jul 2026 10:55:20 +0800 Subject: [PATCH 1/3] Add Platform.System.os_description/0 for backend-safe OS info Replace direct :wx_misc.getOsDescription/0 usage so apps can read a device/OS description on Wx, Json/mobile bridge, and Browser backends without depending on the Hex Bridge GenServer. Co-authored-by: Cursor --- CHANGELOG.md | 1 + guides/faq.md | 2 ++ lib/desktop/backend/browser.ex | 3 ++ lib/desktop/backend/json.ex | 5 ++++ lib/desktop/backend/wx.ex | 5 ++++ lib/desktop/bridge/mock.ex | 1 + lib/desktop/platform/system.ex | 28 +++++++++++++++++++ test/desktop/backend/browser_test.exs | 1 + test/desktop/backend/json_test.exs | 5 ++++ test/desktop/backend/wx_test.exs | 5 ++++ .../desktop/regression/beam_wx_calls_test.exs | 5 ++++ test/support/guard_platform_abstraction.exs | 4 ++- 12 files changed, 64 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b27da79..1e74e6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Optional `config :desktop, :backend, :auto | :wx | :json | :browser` override - Menu adapters: `Desktop.Menu.Adapter.Json` and `Desktop.Menu.Adapter.Browser` - Public `Desktop.*` APIs unchanged (`Desktop.Window`, `Desktop.Env`, `Desktop.Menu`, etc.) +- `Desktop.Platform.System.os_description/0` — backend-safe replacement for `:wx_misc.getOsDescription/0` (Wx, Json bridge, Browser) - 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) diff --git a/guides/faq.md b/guides/faq.md index d73f0bd..1899dae 100644 --- a/guides/faq.md +++ b/guides/faq.md @@ -48,6 +48,8 @@ Inspect what the active backend supports: ```elixir Desktop.Platform.backend() # e.g. Desktop.Backend.Wx Desktop.Platform.capabilities() # %{window: true, content: :webview, ...} +Desktop.Platform.System.locale() +Desktop.Platform.System.os_description() # replaces :wx_misc.getOsDescription/0 ``` | Backend | `window` | `content` | `menu` | diff --git a/lib/desktop/backend/browser.ex b/lib/desktop/backend/browser.ex index 97fa2eb..50c2116 100644 --- a/lib/desktop/backend/browser.ex +++ b/lib/desktop/backend/browser.ex @@ -45,6 +45,9 @@ defmodule Desktop.Backend.Browser do @impl true def open_external_url(url), do: Desktop.Impl.HostBrowser.open(url) + @impl true + def os_description, do: nil + @impl true def activate_event_active?(_event), do: true diff --git a/lib/desktop/backend/json.ex b/lib/desktop/backend/json.ex index 88918f7..f8b4f30 100644 --- a/lib/desktop/backend/json.ex +++ b/lib/desktop/backend/json.ex @@ -77,6 +77,11 @@ defmodule Desktop.Backend.Json do :ok end + @impl true + def os_description do + Protocol.call(:wx_misc, :getOsDescription, []) + end + @impl true def activate_event_active?(_event), do: true diff --git a/lib/desktop/backend/wx.ex b/lib/desktop/backend/wx.ex index 219fbb1..de077cf 100644 --- a/lib/desktop/backend/wx.ex +++ b/lib/desktop/backend/wx.ex @@ -75,6 +75,11 @@ defmodule Desktop.Backend.Wx do end end + @impl true + def os_description do + Null.wx_call(:wx_misc, :getOsDescription, []) + end + @impl true def activate_event_active?(event) do if function_exported?(:wxActivateEvent, :getActive, 1) do diff --git a/lib/desktop/bridge/mock.ex b/lib/desktop/bridge/mock.ex index 9f154fc..4573b42 100644 --- a/lib/desktop/bridge/mock.ex +++ b/lib/desktop/bridge/mock.ex @@ -14,6 +14,7 @@ defmodule Desktop.Bridge.Mock do def handle_method([:wx, :getObjectType, [arg]]), do: Keyword.get(arg, :type) def handle_method([:wxLocale | _]), do: ~c"en" def handle_method([:wx_misc, :launchDefaultBrowser | _]), do: :ok + def handle_method([:wx_misc, :getOsDescription | _]), do: ~c"Mock OS" def handle_method([type, :new | args]), do: [id: System.unique_integer([:positive]), type: type, args: args] diff --git a/lib/desktop/platform/system.ex b/lib/desktop/platform/system.ex index c53876a..9d6ba91 100644 --- a/lib/desktop/platform/system.ex +++ b/lib/desktop/platform/system.ex @@ -17,6 +17,7 @@ defmodule Desktop.Platform.System do term() @callback wx_available?() :: boolean() @callback open_external_url(String.t()) :: :ok + @callback os_description() :: String.t() | charlist() | nil @callback activate_event_active?(event :: term()) :: boolean() def init_env, do: impl().init_env() @@ -32,6 +33,33 @@ defmodule Desktop.Platform.System do def wx_available?, do: impl().wx_available?() def open_external_url(url), do: Helpers.with_wx_env(fn -> impl().open_external_url(url) end) + @doc """ + Returns a human-readable OS / device description string. + + Replaces direct `:wx_misc.getOsDescription/0` calls so apps work on all + backends (Wx, Json/mobile bridge, Browser). Returns `nil` when unavailable. + """ + def os_description do + Helpers.with_wx_env(fn -> + case impl().os_description() do + nil -> nil + desc when is_list(desc) -> List.to_string(desc) + desc when is_binary(desc) -> desc + _ -> nil + end + end) + |> case do + nil -> + nil + + str -> + case String.trim(str) do + "" -> nil + other -> other + end + end + end + def activate_event_active?(event), do: Helpers.with_wx_env(fn -> impl().activate_event_active?(event) end) diff --git a/test/desktop/backend/browser_test.exs b/test/desktop/backend/browser_test.exs index 49c09f6..3e013e9 100644 --- a/test/desktop/backend/browser_test.exs +++ b/test/desktop/backend/browser_test.exs @@ -37,6 +37,7 @@ defmodule Desktop.Backend.BrowserTest do assert :ok = Browser.set_env(nil) assert Browser.get_env() == nil assert Browser.locale() == nil + assert Browser.os_description() == nil refute Browser.wx_available?() end end diff --git a/test/desktop/backend/json_test.exs b/test/desktop/backend/json_test.exs index 9545256..04e5297 100644 --- a/test/desktop/backend/json_test.exs +++ b/test/desktop/backend/json_test.exs @@ -27,6 +27,11 @@ defmodule Desktop.Backend.JsonTest do assert Json.locale() == "en" end + test "T-JSN: os_description via bridge RPC" do + assert Json.os_description() == ~c"Mock OS" + assert Desktop.Platform.System.os_description() == "Mock OS" + end + test "T-JSN: new frame handle shape" do wx = Transport.ensure_started() diff --git a/test/desktop/backend/wx_test.exs b/test/desktop/backend/wx_test.exs index 6bed0db..5fb6260 100644 --- a/test/desktop/backend/wx_test.exs +++ b/test/desktop/backend/wx_test.exs @@ -39,4 +39,9 @@ defmodule Desktop.Backend.WxTest do result = Wx.locale() assert result == nil or is_binary(result) end + + test "T-WX: os_description via Platform.System" do + result = Desktop.Platform.System.os_description() + assert result == nil or is_binary(result) + end end diff --git a/test/desktop/regression/beam_wx_calls_test.exs b/test/desktop/regression/beam_wx_calls_test.exs index 8a5d1ad..3de0832 100644 --- a/test/desktop/regression/beam_wx_calls_test.exs +++ b/test/desktop/regression/beam_wx_calls_test.exs @@ -31,6 +31,11 @@ defmodule Desktop.Regression.BeamWxCallsTest do assert Json.locale() == "en" end + test "os_description uses bridge RPC not OTP :wx_misc" do + assert Json.os_description() == ~c"Mock OS" + assert PlatformSystem.os_description() == "Mock OS" + end + test "open_external_url on Json uses bridge RPC" do assert :ok = Json.open_external_url("https://example.com") assert :ok = PlatformSystem.open_external_url("https://example.com") diff --git a/test/support/guard_platform_abstraction.exs b/test/support/guard_platform_abstraction.exs index 5001b23..3b2ba0c 100644 --- a/test/support/guard_platform_abstraction.exs +++ b/test/support/guard_platform_abstraction.exs @@ -17,7 +17,9 @@ forbidden_patterns = [ {~r/ensure_wx_env/, "use Desktop.Platform.Helpers.with_wx_env/1 via Platform facades, not ensure_wx_env"}, {~r/:wxLocale\.getSystemLanguage\s*\(/, - "Json/mobile: pass :getSystemLanguage as a bridge atom to Protocol, never :wxLocale.getSystemLanguage/0"} + "Json/mobile: pass :getSystemLanguage as a bridge atom to Protocol, never :wxLocale.getSystemLanguage/0"}, + {~r/:wx_misc\.getOsDescription\s*\(/, + "use Desktop.Platform.System.os_description/0 instead of :wx_misc.getOsDescription/0"} ] violations = From 839e27488f85d04c3c87bf5f7b6e4dee9e637383 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 21 Jul 2026 12:20:00 +0000 Subject: [PATCH 2/3] Fix os_description pipe-into-case and cover blank normalization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bind Helpers.with_wx_env/1 before matching so we avoid piping into case, and add Platform.System tests for charlist/binary and blank → nil. Co-authored-by: Dominic Letz --- lib/desktop/platform/system.ex | 21 +++++++------- test/desktop/platform_system_test.exs | 40 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 10 deletions(-) create mode 100644 test/desktop/platform_system_test.exs diff --git a/lib/desktop/platform/system.ex b/lib/desktop/platform/system.ex index 9d6ba91..746f893 100644 --- a/lib/desktop/platform/system.ex +++ b/lib/desktop/platform/system.ex @@ -40,22 +40,23 @@ defmodule Desktop.Platform.System do backends (Wx, Json/mobile bridge, Browser). Returns `nil` when unavailable. """ def os_description do - Helpers.with_wx_env(fn -> - case impl().os_description() do - nil -> nil - desc when is_list(desc) -> List.to_string(desc) - desc when is_binary(desc) -> desc - _ -> nil - end - end) - |> case do + raw = + Helpers.with_wx_env(fn -> + case impl().os_description() do + desc when is_binary(desc) -> desc + desc when is_list(desc) -> List.to_string(desc) + _ -> nil + end + end) + + case raw do nil -> nil str -> case String.trim(str) do "" -> nil - other -> other + trimmed -> trimmed end end end diff --git a/test/desktop/platform_system_test.exs b/test/desktop/platform_system_test.exs new file mode 100644 index 0000000..d2ce3f8 --- /dev/null +++ b/test/desktop/platform_system_test.exs @@ -0,0 +1,40 @@ +defmodule Desktop.Platform.SystemTest do + use ExUnit.Case, async: false + + alias Desktop.Platform.System, as: PlatformSystem + + defmodule StubBackend do + def os_description, do: Process.get(:stub_os_description) + end + + setup do + previous = Application.get_env(:desktop, :backend, :auto) + Application.put_env(:desktop, :backend, StubBackend) + + on_exit(fn -> + Application.put_env(:desktop, :backend, previous) + Process.delete(:stub_os_description) + end) + + :ok + end + + test "os_description normalizes charlist and binary" do + Process.put(:stub_os_description, ~c"Mock OS") + assert PlatformSystem.os_description() == "Mock OS" + + Process.put(:stub_os_description, "Linux Desktop") + assert PlatformSystem.os_description() == "Linux Desktop" + end + + test "os_description trims blank values to nil" do + Process.put(:stub_os_description, " ") + assert PlatformSystem.os_description() == nil + + Process.put(:stub_os_description, ~c"") + assert PlatformSystem.os_description() == nil + + Process.put(:stub_os_description, nil) + assert PlatformSystem.os_description() == nil + end +end From dac0db985bcb74565c0e1380c89d48da88ec4403 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 21 Jul 2026 12:21:43 +0000 Subject: [PATCH 3/3] Make Platform.System stub backend implement set_env/1 wx_use_env/0 calls Platform.System.set_env/1 when Desktop.Env is running, so the os_description stub must no-op set_env/1 for host wx tests. Co-authored-by: Dominic Letz --- test/desktop/platform_system_test.exs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/desktop/platform_system_test.exs b/test/desktop/platform_system_test.exs index d2ce3f8..d6ecb71 100644 --- a/test/desktop/platform_system_test.exs +++ b/test/desktop/platform_system_test.exs @@ -4,6 +4,9 @@ defmodule Desktop.Platform.SystemTest do alias Desktop.Platform.System, as: PlatformSystem defmodule StubBackend do + # Minimal stand-in: Platform.System.os_description/0 uses with_wx_env/1, which may + # call set_env/1 when Desktop.Env is up. + def set_env(_env), do: :ok def os_description, do: Process.get(:stub_os_description) end