From c77686250e7d1ac14e48c5496cf64ca119516407 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Wed, 15 Jul 2026 19:17:21 +0200 Subject: [PATCH] fix: ctrl-q now properly exits --- lib/bds/application.ex | 7 +++++-- lib/bds/tui.ex | 14 +++++++++++++- test/bds/server_test.exs | 5 +++-- test/bds/tui_test.exs | 16 ++++++++++++++++ 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/lib/bds/application.ex b/lib/bds/application.ex index 16634bc..0c83795 100644 --- a/lib/bds/application.ex +++ b/lib/bds/application.ex @@ -13,8 +13,11 @@ defmodule BDS.Application do end # Local TUI: the headless server plus a TUI on the launching terminal, so - # a GUI (via tunnel) and the local terminal can work in parallel. - def mode_children(:tui, env), do: mode_children(:server, env) ++ [{BDS.TUI, []}] + # a GUI (via tunnel) and the local terminal can work in parallel. Quitting + # this TUI stops the whole VM — the terminal is the app in this mode. + # SSH-served TUI sessions never get this flag and stay session-local. + def mode_children(:tui, env), + do: mode_children(:server, env) ++ [{BDS.TUI, [stop_vm_on_exit: true]}] def mode_children(:desktop, env), do: desktop_children(env) diff --git a/lib/bds/tui.ex b/lib/bds/tui.ex index 149d6a2..88e3bf1 100644 --- a/lib/bds/tui.ex +++ b/lib/bds/tui.ex @@ -37,13 +37,17 @@ defmodule BDS.TUI do # ── Mount ──────────────────────────────────────────────────────────────── @impl true - def mount(_opts) do + def mount(opts) do UILocale.put(BDS.Desktop.ShellData.ui_language()) :ok = Events.subscribe() project_id = Projects.shell_snapshot().active_project_id state = %{ + # Set only for the local BDS_MODE=tui child: quitting the TUI then + # shuts the VM down instead of leaving a headless server behind. + stop_vm_on_exit: Keyword.get(opts, :stop_vm_on_exit, false), + stop_fun: Keyword.get(opts, :stop_fun, &System.stop/0), project_id: project_id, view: "posts", sidebar: nil, @@ -60,6 +64,14 @@ defmodule BDS.TUI do {:ok, load_sidebar(state)} end + @impl true + def terminate(_reason, %{stop_vm_on_exit: true, stop_fun: stop_fun}) do + stop_fun.() + :ok + end + + def terminate(_reason, _state), do: :ok + # ── Events: global keys ────────────────────────────────────────────────── @impl true diff --git a/test/bds/server_test.exs b/test/bds/server_test.exs index 5fa8069..582f73d 100644 --- a/test/bds/server_test.exs +++ b/test/bds/server_test.exs @@ -107,9 +107,10 @@ defmodule BDS.ServerTest do end) end - test "tui mode is the server plus a local TUI" do + test "tui mode is the server plus a local TUI that stops the VM on quit" do assert BDS.Application.mode_children(:tui, :prod) == - BDS.Application.mode_children(:server, :prod) ++ [{BDS.TUI, []}] + BDS.Application.mode_children(:server, :prod) ++ + [{BDS.TUI, [stop_vm_on_exit: true]}] end test "desktop mode delegates to desktop_children" do diff --git a/test/bds/tui_test.exs b/test/bds/tui_test.exs index 35a05f4..00409f3 100644 --- a/test/bds/tui_test.exs +++ b/test/bds/tui_test.exs @@ -132,4 +132,20 @@ defmodule BDS.TUITest do test "quit key stops the app" do assert {:stop, _state} = BDS.TUI.handle_event(key("q", ["ctrl"]), mount!()) end + + test "local tui mode stops the VM when the app exits" do + parent = self() + state = mount!(stop_vm_on_exit: true, stop_fun: fn -> send(parent, :vm_stopped) end) + + assert :ok = BDS.TUI.terminate(:normal, state) + assert_receive :vm_stopped + end + + test "ssh-served sessions never stop the VM on exit" do + parent = self() + state = mount!(stop_fun: fn -> send(parent, :vm_stopped) end) + + assert :ok = BDS.TUI.terminate(:normal, state) + refute_receive :vm_stopped, 100 + end end