From 381deb417de73d846ed06b87fdd7d192e0daa01a Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Thu, 16 Jul 2026 17:46:36 +0200 Subject: [PATCH] feat: desktop mode falls back to the TUI when no graphical display is available (issue #33) --- lib/bds/application.ex | 16 +++++++++++++++- lib/bds/server.ex | 25 ++++++++++++++++++++++++- specs/server.allium | 7 ++++++- test/bds/server_test.exs | 26 ++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 3 deletions(-) diff --git a/lib/bds/application.ex b/lib/bds/application.ex index 0c83795..cee6b1f 100644 --- a/lib/bds/application.ex +++ b/lib/bds/application.ex @@ -57,7 +57,7 @@ defmodule BDS.Application do {Task.Supervisor, name: BDS.Scripting.TaskSupervisor}, BDS.Scripting.JobSupervisor, BDS.Embeddings.Index - ] ++ embedding_children() ++ mode_children(BDS.Server.mode(), current_env()) + ] ++ embedding_children() ++ mode_children(boot_mode(), current_env()) opts = [strategy: :one_for_one, name: BDS.Supervisor] Supervisor.start_link(children, opts) @@ -77,6 +77,20 @@ defmodule BDS.Application do Application.get_env(:bds, :current_env_override) || @compiled_env end + # Desktop mode without a graphical display boots the TUI instead + # (issue #33); the log line explains the surprise mode switch. + defp boot_mode do + resolved = BDS.Server.mode() + effective = BDS.Server.effective_mode(resolved) + + if effective != resolved do + require Logger + Logger.info("No graphical display available, starting the terminal UI instead") + end + + effective + end + defp desktop_window_children do if desktop_automation?() do [] diff --git a/lib/bds/server.ex b/lib/bds/server.ex index c4ba44d..86cd99d 100644 --- a/lib/bds/server.ex +++ b/lib/bds/server.ex @@ -28,6 +28,29 @@ defmodule BDS.Server do def mode(nil), do: :desktop + @doc """ + The mode the app actually boots in: the resolved `BDS_MODE`, except that + desktop mode falls back to the TUI when no graphical display is available + (issue #33). Starting the app on a display-less Linux server then still + opens a UI — the terminal one — instead of crashing wx. + + Only non-Darwin unix systems fall back: they need `DISPLAY` or + `WAYLAND_DISPLAY` to run wx, while macOS and Windows always have a + graphical session for a launched app. + """ + @spec effective_mode(:desktop | :server | :tui, {atom(), atom()}, %{ + optional(String.t()) => String.t() + }) :: :desktop | :server | :tui + def effective_mode(mode \\ mode(), os_type \\ :os.type(), env \\ System.get_env()) + + def effective_mode(:desktop, {:unix, os}, env) when os != :darwin do + if Enum.any?(["DISPLAY", "WAYLAND_DISPLAY"], &(env[&1] not in [nil, ""])), + do: :desktop, + else: :tui + end + + def effective_mode(mode, _os_type, _env), do: mode + @doc """ Whether the HTTP endpoint requires the desktop webview auth token. Only desktop mode does: in server/tui mode the endpoint stays loopback-only @@ -35,7 +58,7 @@ defmodule BDS.Server do per-boot webview token would otherwise lock out. """ @spec desktop_auth_required?(:desktop | :server | :tui) :: boolean() - def desktop_auth_required?(mode \\ mode()) + def desktop_auth_required?(mode \\ effective_mode()) def desktop_auth_required?(:desktop), do: true def desktop_auth_required?(_mode), do: false diff --git a/specs/server.allium b/specs/server.allium index 2fba667..205e106 100644 --- a/specs/server.allium +++ b/specs/server.allium @@ -30,7 +30,12 @@ surface ServerRuntimeSurface { rule ModeResolution { when: ApplicationStarted(mode) - -- BDS_MODE environment variable decides the mode once per boot + -- BDS_MODE environment variable decides the mode once per boot. + -- Fallback (issue #33): resolved desktop mode on a non-Darwin unix + -- system without DISPLAY/WAYLAND_DISPLAY boots as tui instead, so a + -- display-less Linux server still opens a UI rather than crashing wx; + -- a log line records the switch. Explicit server/tui modes and + -- macOS/Windows are never rewritten. ensures: BootMode.created(kind: mode) } diff --git a/test/bds/server_test.exs b/test/bds/server_test.exs index 582f73d..b370683 100644 --- a/test/bds/server_test.exs +++ b/test/bds/server_test.exs @@ -23,6 +23,32 @@ defmodule BDS.ServerTest do end end + describe "effective_mode/3" do + test "desktop stays desktop when a display is available" do + assert Server.effective_mode(:desktop, {:unix, :linux}, %{"DISPLAY" => ":0"}) == :desktop + + assert Server.effective_mode(:desktop, {:unix, :linux}, %{"WAYLAND_DISPLAY" => "wayland-0"}) == + :desktop + end + + test "desktop falls back to the TUI on unix without a display" do + assert Server.effective_mode(:desktop, {:unix, :linux}, %{}) == :tui + assert Server.effective_mode(:desktop, {:unix, :linux}, %{"DISPLAY" => ""}) == :tui + assert Server.effective_mode(:desktop, {:unix, :freebsd}, %{"WAYLAND_DISPLAY" => ""}) == :tui + end + + test "macOS and Windows never fall back" do + assert Server.effective_mode(:desktop, {:unix, :darwin}, %{}) == :desktop + assert Server.effective_mode(:desktop, {:win32, :nt}, %{}) == :desktop + end + + test "explicit server and tui modes are untouched" do + assert Server.effective_mode(:server, {:unix, :linux}, %{}) == :server + assert Server.effective_mode(:tui, {:unix, :linux}, %{"DISPLAY" => ":0"}) == :tui + assert Server.effective_mode(:server, {:unix, :darwin}, %{"DISPLAY" => ":0"}) == :server + end + end + describe "ensure_ssh_dir!/1" do setup do dir = Path.join(System.tmp_dir!(), "bds-ssh-test-#{System.unique_integer([:positive])}")