idea: TUI for the application as an alternative entry #26

Closed
opened 2026-07-14 14:33:03 +00:00 by hugo · 12 comments
Owner

the app should have an additional entry (maybe like a cli tool) that starts a terminal user interface, supporting the same functionality mostly (aside from clearly graphical elements) to at least work with the blog in a structured manner, create new entries and manage publishing work flow.

Editing of text shoud preferably be also done inside the app directly, using the ExRatatui widgets to manage the text. Maybe a variation of the rich text widget can be used. Or maybe even better the normal textarea widget, but with ratatui-code-editor support, since that is most similar to what we already do in the normal GUI applicaiton.

Also images should be supported via the image widget in ExRatatui, so that we can at least get a glimpse of the media we select (since they are highly visual objects).

The terminal user interface should try to follow most UI flows from the original app. Not 100% feature parity, but preferably close to that, reusing the liveview backend code and only replacing the liveview UI parts with ExRatatui.

The original app backend code needs to be separated out into libraries probably, so that we can do a proper backend server that can then be connected from a UI client remotly, so the app could run headless on a VPS and be managed from different terminals (local and remote). This would also allow integration of terminal command shortcuts to feed data into a running instance of bds2 on a remote machine.

As part of this idea, also the GUI code should be refactored so that we have the same client/server separation as with the TUI, so that we can run headless servers and connect to them via GUI or TUI and even in parallel. This of course means that there needs to be some pub/sub pattern so that the clients get notified of central changes, so that multiple clients are synchronized in what they show.

the app should have an additional entry (maybe like a cli tool) that starts a terminal user interface, supporting the same functionality mostly (aside from clearly graphical elements) to at least work with the blog in a structured manner, create new entries and manage publishing work flow. Editing of text shoud preferably be also done inside the app directly, using the ExRatatui widgets to manage the text. Maybe a variation of the rich text widget can be used. Or maybe even better the normal textarea widget, but with ratatui-code-editor support, since that is most similar to what we already do in the normal GUI applicaiton. Also images should be supported via the image widget in ExRatatui, so that we can at least get a glimpse of the media we select (since they are highly visual objects). The terminal user interface should try to follow most UI flows from the original app. Not 100% feature parity, but preferably close to that, reusing the liveview backend code and only replacing the liveview UI parts with ExRatatui. The original app backend code needs to be separated out into libraries probably, so that we can do a proper backend server that can then be connected from a UI client remotly, so the app could run headless on a VPS and be managed from different terminals (local and remote). This would also allow integration of terminal command shortcuts to feed data into a running instance of bds2 on a remote machine. As part of this idea, also the GUI code should be refactored so that we have the same client/server separation as with the TUI, so that we can run headless servers and connect to them via GUI or TUI and even in parallel. This of course means that there needs to be some pub/sub pattern so that the clients get notified of central changes, so that multiple clients are synchronized in what they show.
hugo added the idea label 2026-07-14 15:23:19 +00:00
Author
Owner

Implementation plan (agreed 2026-07-14)

Architecture: single OTP app, offline-first, two boot modes on all platforms (macOS/Windows/Linux; Linux servers first).

  • desktop = today, unchanged.
  • server = headless: contexts + Repo + PubSub + LiveView endpoint bound to 127.0.0.1 + ExRatatui.SSH.Daemon (public-key auth only: authorized_keys + auto_host_key in app-support dir, tcpip_tunnel_out: true).

Single user, multiple clients (multi-location, not multi-user): all clients share the same project, settings, and workbench structure. Only the SSH port is ever exposed — TUI and GUI both go through it. No umbrella/library split, no custom wire protocol: ExRatatui-over-SSH serves the TUI (app instances run server-side, only terminal cells cross the wire), and the GUI tunnels to the localhost LiveView endpoint through the same SSH daemon with the same keys.

Phases

Phase 1 — headless boot + SSH daemon. bds server entry; wx-free supervision tree (extend desktop_children/1); ExRatatui.SSH.Daemon child; endpoint forced to localhost in server mode; check_origin for tunneled localhost. Headless works on all three OSes (no wx started at all).

Phase 2 — domain event bus. Phoenix.PubSub broadcasts from contexts (Posts, Media, Publishing, Tags, Templates, Settings); all clients subscribe. Enables: multi-client sync, external pipeline ingestion updating the GUI live (incl. existing CliSync.Watcher path), and the server-side UI-language rule (language is a server setting; changing it broadcasts and every client re-renders). New specs/events.allium.

Phase 3 — UI core extraction. Move remaining shell_live/* logic into BDS.UI.* pure command→state+effects functions; LiveView and TUI become thin renderers over one shared core and one shared workbench session. Per-editor, test-first.

Phase 4 — TUI app. ExRatatui.App over Workbench/Sidebar/Dashboard: List/Table sidebar, Tabs strip, Textarea post editing (plain text in v1, see follow-ups), Markdown highlighted preview pane, Widgets.Image for media glimpses, status line + Throbber as toast equivalent. AI quick actions (auto-translate, alt-text, one-shots) included and airplane-mode gated exactly like the GUI; no chat panel. Local bds tui and remote ssh hit the same app module. Tests drive the TUI via ExRatatui.CellSession; new specs/tui.allium, weeded against code.

Phase 5 — GUI remote mode. Desktop app "Connect to server…": :ssh.connect with user key → :ssh.tcpip_tunnel_to_server → webview at the local tunnel port. Native menu entry via macOS intercept, localized. Local desktop mode stays the offline-first default. Plain browsers work too via standard ssh -L.

Phase 6 — gates. gettext for all new strings (de/fr/it/es, real translations, both localization areas), DOCUMENTATION.md server-setup + key-management section, clean build/test/credo/deps.audit/dialyzer. No new metadata fields.

Syntax-highlighted editing (deferred from v1)

ExRatatui 0.11 has no ratatui-code-editor integration: Textarea wraps ratatui-textarea (no highlighting), CodeBlock is display-only, and custom widgets are pure-Elixir compositions — external Rust cannot draw into the NIF's buffer without forking it. Since only markdown highlighting is needed, the preferred follow-up is a pure-Elixir MarkdownEditor custom widget (own line buffer/cursor/undo, per-line styled Text.Spans from an mdex-based tokenizer — no fork, no Rust, CellSession-testable). An upstream PR wrapping ratatui-code-editor remains the long-term option if the Elixir widget hits a ceiling.

Follow-ups (separate issues)

  • Markdown-highlighted editor widget (above).
  • Terminal command shortcuts to feed a running instance (covered by its own issue).
# Implementation plan (agreed 2026-07-14) **Architecture:** single OTP app, offline-first, two boot modes on all platforms (macOS/Windows/Linux; Linux servers first). - `desktop` = today, unchanged. - `server` = headless: contexts + Repo + PubSub + LiveView endpoint bound to 127.0.0.1 + `ExRatatui.SSH.Daemon` (public-key auth only: `authorized_keys` + `auto_host_key` in app-support dir, `tcpip_tunnel_out: true`). Single user, multiple clients (multi-location, not multi-user): all clients share the same project, settings, and workbench structure. Only the SSH port is ever exposed — TUI and GUI both go through it. No umbrella/library split, no custom wire protocol: ExRatatui-over-SSH serves the TUI (app instances run server-side, only terminal cells cross the wire), and the GUI tunnels to the localhost LiveView endpoint through the same SSH daemon with the same keys. ## Phases **Phase 1 — headless boot + SSH daemon.** `bds server` entry; wx-free supervision tree (extend `desktop_children/1`); `ExRatatui.SSH.Daemon` child; endpoint forced to localhost in server mode; `check_origin` for tunneled localhost. Headless works on all three OSes (no wx started at all). **Phase 2 — domain event bus.** `Phoenix.PubSub` broadcasts from contexts (`Posts`, `Media`, `Publishing`, `Tags`, `Templates`, `Settings`); all clients subscribe. Enables: multi-client sync, external pipeline ingestion updating the GUI live (incl. existing `CliSync.Watcher` path), and the server-side UI-language rule (language is a server setting; changing it broadcasts and every client re-renders). New `specs/events.allium`. **Phase 3 — UI core extraction.** Move remaining `shell_live/*` logic into `BDS.UI.*` pure command→state+effects functions; LiveView and TUI become thin renderers over one shared core and one shared workbench session. Per-editor, test-first. **Phase 4 — TUI app.** `ExRatatui.App` over Workbench/Sidebar/Dashboard: List/Table sidebar, Tabs strip, `Textarea` post editing (plain text in v1, see follow-ups), `Markdown` highlighted preview pane, `Widgets.Image` for media glimpses, status line + Throbber as toast equivalent. AI quick actions (auto-translate, alt-text, one-shots) included and airplane-mode gated exactly like the GUI; no chat panel. Local `bds tui` and remote `ssh` hit the same app module. Tests drive the TUI via `ExRatatui.CellSession`; new `specs/tui.allium`, weeded against code. **Phase 5 — GUI remote mode.** Desktop app "Connect to server…": `:ssh.connect` with user key → `:ssh.tcpip_tunnel_to_server` → webview at the local tunnel port. Native menu entry via macOS intercept, localized. Local desktop mode stays the offline-first default. Plain browsers work too via standard `ssh -L`. **Phase 6 — gates.** gettext for all new strings (de/fr/it/es, real translations, both localization areas), DOCUMENTATION.md server-setup + key-management section, clean build/test/credo/deps.audit/dialyzer. No new metadata fields. ## Syntax-highlighted editing (deferred from v1) ExRatatui 0.11 has no ratatui-code-editor integration: `Textarea` wraps `ratatui-textarea` (no highlighting), `CodeBlock` is display-only, and custom widgets are pure-Elixir compositions — external Rust cannot draw into the NIF's buffer without forking it. Since only *markdown* highlighting is needed, the preferred follow-up is a pure-Elixir `MarkdownEditor` custom widget (own line buffer/cursor/undo, per-line styled `Text.Span`s from an mdex-based tokenizer — no fork, no Rust, `CellSession`-testable). An upstream PR wrapping ratatui-code-editor remains the long-term option if the Elixir widget hits a ceiling. ## Follow-ups (separate issues) - Markdown-highlighted editor widget (above). - Terminal command shortcuts to feed a running instance (covered by its own issue).
Author
Owner

Phase 1 finished (commit 700ed79 on feature/issue-26-tui-headless-server)

Headless boot mode + SSH daemon:

  • BDS_MODE=server boots the app with no wx children at all (works headless on macOS/Windows/Linux): loopback HTTP endpoint + CLI sync watcher + ExRatatui.SSH.Daemon.
  • The daemon serves BDS.TUI (placeholder shell for now; the real workbench renderer lands in phase 4) — every SSH client gets its own app instance on the server.
  • Auth is public-key only (auth_methods: publickey, no password callback). Host key is generated on first boot; authorized_keys is created empty with mode 600. All key material lives in <data dir>/ssh next to the database — never in the repo or priv.
  • tcpip_tunnel_out is enabled so GUI clients can tunnel to the loopback endpoint through the same SSH connection (phase 5).
  • SSH port: BDS_SSH_PORT env or config :bds, :server, ssh_port: (default 2222).
  • New specs/server.allium (validates clean), 11 new tests (mode resolution, key-material handling, daemon opts, mode children), new TUI strings translated for de/fr/it/es.
  • Also fixed a pre-existing load-flake in BDS.Scripting.JobTest (1s → 5s cleanup timeout).

Gates: build (warnings-as-errors) , full suite 1314 tests / 0 failures after flake fix , credo --strict , deps.audit , dialyzer .

Next: phase 2 — domain event bus on BDS.PubSub.

**Phase 1 finished** (commit 700ed79 on `feature/issue-26-tui-headless-server`) Headless boot mode + SSH daemon: - `BDS_MODE=server` boots the app with no wx children at all (works headless on macOS/Windows/Linux): loopback HTTP endpoint + CLI sync watcher + `ExRatatui.SSH.Daemon`. - The daemon serves `BDS.TUI` (placeholder shell for now; the real workbench renderer lands in phase 4) — every SSH client gets its own app instance on the server. - Auth is public-key only (`auth_methods: publickey`, no password callback). Host key is generated on first boot; `authorized_keys` is created empty with mode 600. All key material lives in `<data dir>/ssh` next to the database — never in the repo or priv. - `tcpip_tunnel_out` is enabled so GUI clients can tunnel to the loopback endpoint through the same SSH connection (phase 5). - SSH port: `BDS_SSH_PORT` env or `config :bds, :server, ssh_port:` (default 2222). - New `specs/server.allium` (validates clean), 11 new tests (mode resolution, key-material handling, daemon opts, mode children), new TUI strings translated for de/fr/it/es. - Also fixed a pre-existing load-flake in `BDS.Scripting.JobTest` (1s → 5s cleanup timeout). Gates: build (warnings-as-errors) ✅, full suite 1314 tests / 0 failures after flake fix ✅, credo --strict ✅, deps.audit ✅, dialyzer ✅. Next: phase 2 — domain event bus on `BDS.PubSub`.
Author
Owner

Phase 2 finished (commit 16f45d7 on feature/issue-26-tui-headless-server)

Domain event bus:

  • New BDS.Events: contexts broadcast {:entity_changed, %{entity, entity_id, action}} on the same "entity:changed" topic the CLI sync watcher already uses, so one subscription covers in-app mutations, other clients, and external CLI/pipeline writes. Global settings writes broadcast {:settings_changed, key} on "settings:changed".
  • Instrumented every successful mutation in Posts (create/update/publish/delete), Media, Tags (incl. rename/merge), Templates, Scripts, and Settings.put_global_setting.
  • ShellLive now subscribes via BDS.Events.subscribe() — two GUI windows (and later TUI sessions) stay synchronized; a pipeline ingesting content outside the GUI updates every open shell live.
  • UI language is now a server-side setting (ui.language): changing it in any client persists it and re-localizes every connected client via the broadcast. OS locale (LC_ALL/LANG) remains the fallback for a fresh install.
  • Kept the query-count performance guards green: ShellData.status_bar/4 uses Keyword.get_lazy (its eagerly-evaluated default was re-reading the setting on every layout event) and editor_meta/2 takes the caller's already-resolved locale.
  • New specs/events.allium (validates clean); 7 new tests covering broadcasts across all contexts and the ui.language override.

Gates: build (warnings-as-errors) , full suite 0 failures , credo --strict , deps.audit , dialyzer .

Next: phase 3 — extract remaining pure post-editor logic into BDS.UI.PostEditor.* (the socket-facing half of DraftManagement turned out to be dead code and gets deleted).

**Phase 2 finished** (commit 16f45d7 on `feature/issue-26-tui-headless-server`) Domain event bus: - New `BDS.Events`: contexts broadcast `{:entity_changed, %{entity, entity_id, action}}` on the same `"entity:changed"` topic the CLI sync watcher already uses, so one subscription covers in-app mutations, other clients, and external CLI/pipeline writes. Global settings writes broadcast `{:settings_changed, key}` on `"settings:changed"`. - Instrumented every successful mutation in `Posts` (create/update/publish/delete), `Media`, `Tags` (incl. rename/merge), `Templates`, `Scripts`, and `Settings.put_global_setting`. - `ShellLive` now subscribes via `BDS.Events.subscribe()` — two GUI windows (and later TUI sessions) stay synchronized; a pipeline ingesting content outside the GUI updates every open shell live. - UI language is now a server-side setting (`ui.language`): changing it in any client persists it and re-localizes every connected client via the broadcast. OS locale (`LC_ALL`/`LANG`) remains the fallback for a fresh install. - Kept the query-count performance guards green: `ShellData.status_bar/4` uses `Keyword.get_lazy` (its eagerly-evaluated default was re-reading the setting on every layout event) and `editor_meta/2` takes the caller's already-resolved locale. - New `specs/events.allium` (validates clean); 7 new tests covering broadcasts across all contexts and the ui.language override. Gates: build (warnings-as-errors) ✅, full suite 0 failures ✅, credo --strict ✅, deps.audit ✅, dialyzer ✅. Next: phase 3 — extract remaining pure post-editor logic into `BDS.UI.PostEditor.*` (the socket-facing half of `DraftManagement` turned out to be dead code and gets deleted).
Author
Owner

Phase 3 finished (commit 3a8fafd on feature/issue-26-tui-headless-server)

UI core extraction:

  • Survey result: the workbench core was already renderer-agnostic (BDS.UI.Workbench, Sidebar, Dashboard, Commands, Session have zero LiveView imports), so the real gap was the post editor.
  • Moved the LiveView-free post-editor logic into BDS.UI.PostEditor.*: Persistence (save/publish/discard workflow incl. canonical-vs-translation routing), Metadata (project metadata, translations, template options, linked media, post links, footer), ListValues (tag/category chips + suggestions), and new Draft (persisted form building, param normalization, language handling).
  • BDS.Desktop.ShellLive.PostEditor now imports from BDS.UI.PostEditor.*; the TUI (phase 4) drives the exact same save/publish/discard code paths without touching the Desktop namespace.
  • Deleted DraftManagement's socket-facing half entirely — it turned out to be dead code (defined, never called; the LiveComponent had its own equivalents). Net −66 lines.
  • New workflow tests exercise the extracted core against real contexts: form building, edit→save→publish, discard, param normalization.

Gates: build (warnings-as-errors) , full suite 1325 tests / 0 failures , credo --strict , deps.audit , dialyzer .

Next: phase 4 — the TUI itself (ExRatatui.App over the shared core).

**Phase 3 finished** (commit 3a8fafd on `feature/issue-26-tui-headless-server`) UI core extraction: - Survey result: the workbench core was already renderer-agnostic (`BDS.UI.Workbench`, `Sidebar`, `Dashboard`, `Commands`, `Session` have zero LiveView imports), so the real gap was the post editor. - Moved the LiveView-free post-editor logic into `BDS.UI.PostEditor.*`: `Persistence` (save/publish/discard workflow incl. canonical-vs-translation routing), `Metadata` (project metadata, translations, template options, linked media, post links, footer), `ListValues` (tag/category chips + suggestions), and new `Draft` (persisted form building, param normalization, language handling). - `BDS.Desktop.ShellLive.PostEditor` now imports from `BDS.UI.PostEditor.*`; the TUI (phase 4) drives the exact same save/publish/discard code paths without touching the Desktop namespace. - Deleted `DraftManagement`'s socket-facing half entirely — it turned out to be dead code (defined, never called; the LiveComponent had its own equivalents). Net −66 lines. - New workflow tests exercise the extracted core against real contexts: form building, edit→save→publish, discard, param normalization. Gates: build (warnings-as-errors) ✅, full suite 1325 tests / 0 failures ✅, credo --strict ✅, deps.audit ✅, dialyzer ✅. Next: phase 4 — the TUI itself (`ExRatatui.App` over the shared core).
Author
Owner

Phase 4 finished (commit a3ca446 on feature/issue-26-tui-headless-server)

Terminal UI:

  • BDS.TUI is now a real ExRatatui.App over the shared core: sidebar from BDS.UI.Sidebar (same data as the GUI, grouped sections with header-skipping navigation), view tabs (posts/media/templates/scripts/tags via 1-5), and a status line with contextual key hints.
  • Post editing: enter opens a post with title + Textarea body seeded from BDS.UI.PostEditor.Draft.persisted_form; ctrl+s saves and ctrl+p publishes through BDS.UI.PostEditor.Persistence — the exact code paths the GUI uses (files, embeddings, search, auto-translation, event bus all behave identically). ctrl+t edits the title, ctrl+l cycles languages (canonical vs translations), n creates a draft.
  • Media glimpse: enter on an image renders it in the terminal via ExRatatui.Widgets.Image (kitty/sixel/halfblocks auto-negotiated), esc closes.
  • AI quick action: ctrl+g runs the one-shot AI.analyze_post (title/excerpt suggestions applied to the post), airplane-mode gated — without a local endpoint it shows a status message instead of calling out.
  • Multi-client sync: TUI sessions subscribe to BDS.Events — sidebar refreshes on any entity change from any client or pipeline; a post deleted elsewhere closes the open editor; ui.language changes re-localize the session (server-side locale, shared by all clients).
  • New boot mode BDS_MODE=tui: the headless server plus a TUI on the launching terminal (SSH daemon still runs, so GUI/TUI clients can attach in parallel).
  • 13 TUI tests drive the app headlessly via ExRatatui.CellSession (render-to-cells assertions + real context persistence); new specs/tui.allium (validates clean); all 18 new UI strings translated for de/fr/it/es, plus two pre-existing fuzzy entries fixed ("Media Files", "Delete Tag").
  • README gained a "Headless Server Mode And Terminal UI" section (build, run, keys, remote GUI tunnel).

Skipped for v1 (as planned): syntax-highlighted editing (follow-up issue — plain textarea now), Throbber widget (text "Working…" status covers it), chat panel.

Gates: build (warnings-as-errors) , full suite 1336 tests / 0 failures , credo --strict , deps.audit , dialyzer .

Next: phase 5 — desktop "Connect to Server…" over the same SSH channel.

**Phase 4 finished** (commit a3ca446 on `feature/issue-26-tui-headless-server`) Terminal UI: - `BDS.TUI` is now a real `ExRatatui.App` over the shared core: sidebar from `BDS.UI.Sidebar` (same data as the GUI, grouped sections with header-skipping navigation), view tabs (posts/media/templates/scripts/tags via `1-5`), and a status line with contextual key hints. - Post editing: `enter` opens a post with title + `Textarea` body seeded from `BDS.UI.PostEditor.Draft.persisted_form`; `ctrl+s` saves and `ctrl+p` publishes through `BDS.UI.PostEditor.Persistence` — the exact code paths the GUI uses (files, embeddings, search, auto-translation, event bus all behave identically). `ctrl+t` edits the title, `ctrl+l` cycles languages (canonical vs translations), `n` creates a draft. - Media glimpse: `enter` on an image renders it in the terminal via `ExRatatui.Widgets.Image` (kitty/sixel/halfblocks auto-negotiated), `esc` closes. - AI quick action: `ctrl+g` runs the one-shot `AI.analyze_post` (title/excerpt suggestions applied to the post), airplane-mode gated — without a local endpoint it shows a status message instead of calling out. - Multi-client sync: TUI sessions subscribe to `BDS.Events` — sidebar refreshes on any entity change from any client or pipeline; a post deleted elsewhere closes the open editor; `ui.language` changes re-localize the session (server-side locale, shared by all clients). - New boot mode `BDS_MODE=tui`: the headless server plus a TUI on the launching terminal (SSH daemon still runs, so GUI/TUI clients can attach in parallel). - 13 TUI tests drive the app headlessly via `ExRatatui.CellSession` (render-to-cells assertions + real context persistence); new `specs/tui.allium` (validates clean); all 18 new UI strings translated for de/fr/it/es, plus two pre-existing fuzzy entries fixed ("Media Files", "Delete Tag"). - README gained a "Headless Server Mode And Terminal UI" section (build, run, keys, remote GUI tunnel). Skipped for v1 (as planned): syntax-highlighted editing (follow-up issue — plain textarea now), Throbber widget (text "Working…" status covers it), chat panel. Gates: build (warnings-as-errors) ✅, full suite 1336 tests / 0 failures ✅, credo --strict ✅, deps.audit ✅, dialyzer ✅. Next: phase 5 — desktop "Connect to Server…" over the same SSH channel.
Author
Owner

Phase 5 finished (commit df2bdc2 on feature/issue-26-tui-headless-server)

GUI remote mode over the same SSH channel:

  • New BDS.Desktop.RemoteConnection: parses user@host[:port], connects with OTP :ssh using public-key auth only (client identity + trust-on-first-use known_hosts in the same private ssh/ dir the server uses), then opens an OTP TCP/IP tunnel (:ssh.tcpip_tunnel_to_server/6) to the server's loopback HTTP endpoint. The webview loads the local tunnel end — the server's HTTP port is never exposed. Connection loss is monitored and reported.
  • File → Connect to Server… (native macOS dialog) and File → Disconnect from Server menu items, wired both natively (BDS.Desktop.MenuBar) and through the shell dispatcher for the non-macOS titlebar menu; disconnect returns the window to the local workspace.
  • Server side: the endpoint skips the per-boot webview auth token outside desktop mode (it would lock out tunneled clients; the SSH keys are the auth, and the endpoint stays loopback-only). check_origin now accepts any-port loopback origins because the tunnel terminates on an ephemeral local port.
  • New BDS.Desktop.Dialogs (native osascript text prompt + alert, same pattern as the file picker).
  • 9 new tests (target parsing, connect/tunnel/disconnect against injected ssh fakes, menu coverage, auth-mode matrix); specs/server.allium gained the GuiRemoteConnection rule; all 6 new UI strings translated for de/fr/it/es; README remote-GUI section updated.

Gates: build (warnings-as-errors) , full suite 0 failures , credo --strict , deps.audit , dialyzer .

Next: phase 6 — final gates + end-user documentation (server setup and key management in DOCUMENTATION.md).

**Phase 5 finished** (commit df2bdc2 on `feature/issue-26-tui-headless-server`) GUI remote mode over the same SSH channel: - New `BDS.Desktop.RemoteConnection`: parses `user@host[:port]`, connects with OTP `:ssh` using public-key auth only (client identity + trust-on-first-use `known_hosts` in the same private `ssh/` dir the server uses), then opens an OTP TCP/IP tunnel (`:ssh.tcpip_tunnel_to_server/6`) to the server's loopback HTTP endpoint. The webview loads the local tunnel end — the server's HTTP port is never exposed. Connection loss is monitored and reported. - **File → Connect to Server…** (native macOS dialog) and **File → Disconnect from Server** menu items, wired both natively (`BDS.Desktop.MenuBar`) and through the shell dispatcher for the non-macOS titlebar menu; disconnect returns the window to the local workspace. - Server side: the endpoint skips the per-boot webview auth token outside desktop mode (it would lock out tunneled clients; the SSH keys are the auth, and the endpoint stays loopback-only). `check_origin` now accepts any-port loopback origins because the tunnel terminates on an ephemeral local port. - New `BDS.Desktop.Dialogs` (native osascript text prompt + alert, same pattern as the file picker). - 9 new tests (target parsing, connect/tunnel/disconnect against injected ssh fakes, menu coverage, auth-mode matrix); `specs/server.allium` gained the `GuiRemoteConnection` rule; all 6 new UI strings translated for de/fr/it/es; README remote-GUI section updated. Gates: build (warnings-as-errors) ✅, full suite 0 failures ✅, credo --strict ✅, deps.audit ✅, dialyzer ✅. Next: phase 6 — final gates + end-user documentation (server setup and key management in DOCUMENTATION.md).
Author
Owner

Phase 6 finished (commit 48e0903 on feature/issue-26-tui-headless-server) — issue #26 implementation complete.

Docs + final gates:

  • DOCUMENTATION.md gained "Running a headless server and the terminal UI": end-user guidance for BDS_MODE=server, key management (authorized_keys in the private ssh/ dir), connecting via ssh (TUI), File → Connect to Server… (GUI), browser-over-tunnel, and the multi-client sync / shared-language behavior.
  • API.md: app.get_system_language description updated for the new semantics (server-side UI language setting, OS locale fallback) and regenerated from the live contract (docs sync tests green).
  • No new metadata fields were introduced anywhere in this issue, so publishing/metadata-diff/rebuild stay untouched by design.
  • Final gates on the finished branch: build (warnings-as-errors) , full suite 1345+ tests / 0 failures , credo --strict , deps.audit , dialyzer . All four locales complete (no empty or fuzzy msgstr in de/fr/it/es).

Summary of the whole implementation (phases 1–6):

  1. Headless boot (BDS_MODE=server) + ExRatatui.SSH.Daemon, public-key-only auth, key material in the private data dir, tcpip_tunnel_out for GUI clients.
  2. Domain event bus (BDS.Events) — all contexts broadcast; all clients synchronized; UI language became a server-side setting.
  3. Pure post-editor core extracted to BDS.UI.PostEditor.*; dead socket-half of DraftManagement deleted.
  4. Terminal UI (BDS.TUI) over the shared core: posts/media/templates/scripts/tags, post editing + publish, image preview, airplane-gated AI quick action, BDS_MODE=tui local mode, CellSession-driven tests.
  5. Desktop Connect to Server…: OTP ssh + TCP/IP tunnel to the loopback endpoint, same keys as the TUI, native + titlebar menu wiring.
  6. Docs (README, DOCUMENTATION.md, API.md) + clean final gates.

Deferred to follow-ups (as agreed): markdown-syntax-highlighted editor widget (pure-Elixir custom widget preferred; upstream ratatui-code-editor PR as the long-term option), terminal command shortcuts (covered by its own issue).

Branch feature/issue-26-tui-headless-server is ready for review/merge.

**Phase 6 finished** (commit 48e0903 on `feature/issue-26-tui-headless-server`) — issue #26 implementation complete. Docs + final gates: - DOCUMENTATION.md gained "Running a headless server and the terminal UI": end-user guidance for `BDS_MODE=server`, key management (`authorized_keys` in the private `ssh/` dir), connecting via `ssh` (TUI), **File → Connect to Server…** (GUI), browser-over-tunnel, and the multi-client sync / shared-language behavior. - API.md: `app.get_system_language` description updated for the new semantics (server-side UI language setting, OS locale fallback) and regenerated from the live contract (docs sync tests green). - No new metadata fields were introduced anywhere in this issue, so publishing/metadata-diff/rebuild stay untouched by design. - Final gates on the finished branch: build (warnings-as-errors) ✅, full suite 1345+ tests / 0 failures ✅, credo --strict ✅, deps.audit ✅, dialyzer ✅. All four locales complete (no empty or fuzzy msgstr in de/fr/it/es). **Summary of the whole implementation (phases 1–6):** 1. Headless boot (`BDS_MODE=server`) + `ExRatatui.SSH.Daemon`, public-key-only auth, key material in the private data dir, `tcpip_tunnel_out` for GUI clients. 2. Domain event bus (`BDS.Events`) — all contexts broadcast; all clients synchronized; UI language became a server-side setting. 3. Pure post-editor core extracted to `BDS.UI.PostEditor.*`; dead socket-half of `DraftManagement` deleted. 4. Terminal UI (`BDS.TUI`) over the shared core: posts/media/templates/scripts/tags, post editing + publish, image preview, airplane-gated AI quick action, `BDS_MODE=tui` local mode, CellSession-driven tests. 5. Desktop **Connect to Server…**: OTP ssh + TCP/IP tunnel to the loopback endpoint, same keys as the TUI, native + titlebar menu wiring. 6. Docs (README, DOCUMENTATION.md, API.md) + clean final gates. **Deferred to follow-ups (as agreed):** markdown-syntax-highlighted editor widget (pure-Elixir custom widget preferred; upstream ratatui-code-editor PR as the long-term option), terminal command shortcuts (covered by its own issue). Branch `feature/issue-26-tui-headless-server` is ready for review/merge.
hugo closed this issue 2026-07-15 17:16:57 +00:00
Author
Owner

Post-phase fix (commit c776862): quitting the local BDS_MODE=tui TUI (ctrl+q) now stops the whole VM instead of leaving the headless server running in the foreground. The local TUI child gets stop_vm_on_exit: true and BDS.TUI.terminate/2 calls System.stop() — SSH-served sessions never get the flag, so remote quits stay session-local. Covered by 3 new/updated tests; full suite 1347/0, all gates green.

**Post-phase fix** (commit c776862): quitting the local `BDS_MODE=tui` TUI (ctrl+q) now stops the whole VM instead of leaving the headless server running in the foreground. The local TUI child gets `stop_vm_on_exit: true` and `BDS.TUI.terminate/2` calls `System.stop()` — SSH-served sessions never get the flag, so remote quits stay session-local. Covered by 3 new/updated tests; full suite 1347/0, all gates green.
Author
Owner

Post-phase addition (commit 01b90e0): word-wrapped preview in the TUI post editor. The editing textarea cannot soft-wrap — upstream ratatui-textarea limitation, no wrap option in the crate or the ExRatatui NIF surface — so ctrl+e now toggles a read-only, word-wrapped Markdown rendering of the current draft (esc or ctrl+e returns to editing; keys in preview never touch the content). Proper soft-wrap inside the editing surface is folded into the planned MarkdownEditor custom-widget follow-up as a hard requirement alongside syntax highlighting. 3 new CellSession tests (wrap visibility, edit protection, esc behavior); spec + README + all four locales updated. Full suite 1350/0, credo/deps.audit/dialyzer clean.

**Post-phase addition** (commit 01b90e0): word-wrapped preview in the TUI post editor. The editing textarea cannot soft-wrap — upstream `ratatui-textarea` limitation, no wrap option in the crate or the ExRatatui NIF surface — so `ctrl+e` now toggles a read-only, word-wrapped Markdown rendering of the current draft (`esc` or `ctrl+e` returns to editing; keys in preview never touch the content). Proper soft-wrap inside the editing surface is folded into the planned MarkdownEditor custom-widget follow-up as a hard requirement alongside syntax highlighting. 3 new CellSession tests (wrap visibility, edit protection, esc behavior); spec + README + all four locales updated. Full suite 1350/0, credo/deps.audit/dialyzer clean.
Author
Owner

Post-phase addition (commit 824313f): vi-style command prompt in the TUI. : opens a filterable list of the parameterless Blog-menu shell commands — metadata diff, validate site, force render site, generate site, rebuild database, reindex text, rebuild embedding index, regenerate calendar, validate/fill translations, find duplicates, upload site, open-in-browser (URL shown) — and runs the selection through the same BDS.Desktop.ShellCommands backend the GUI menu uses. ? (or :?) shows the same list as help. Commands whose report/apply follow-up UI exists only in the GUI (metadata diff, validate site, validate translations, find duplicates) are marked [report/apply in GUI]. Queued tasks stream progress into the status line until all tasks finish, then the sidebar refreshes. The parameterized follow-ups (repair_metadata_diff, import_metadata_diff_orphans, apply_site_validation) need report selections and stay GUI-only until a TUI report pane lands (natural next increment). 6 new tests; spec/README/all four locales updated. Full suite 1356/0, credo/deps.audit/dialyzer clean.

**Post-phase addition** (commit 824313f): vi-style command prompt in the TUI. `:` opens a filterable list of the parameterless Blog-menu shell commands — metadata diff, validate site, force render site, generate site, rebuild database, reindex text, rebuild embedding index, regenerate calendar, validate/fill translations, find duplicates, upload site, open-in-browser (URL shown) — and runs the selection through the same `BDS.Desktop.ShellCommands` backend the GUI menu uses. `?` (or `:?`) shows the same list as help. Commands whose report/apply follow-up UI exists only in the GUI (metadata diff, validate site, validate translations, find duplicates) are marked `[report/apply in GUI]`. Queued tasks stream progress into the status line until all tasks finish, then the sidebar refreshes. The parameterized follow-ups (`repair_metadata_diff`, `import_metadata_diff_orphans`, `apply_site_validation`) need report selections and stay GUI-only until a TUI report pane lands (natural next increment). 6 new tests; spec/README/all four locales updated. Full suite 1356/0, credo/deps.audit/dialyzer clean.
Author
Owner

Post-phase addition (commit 39bb232): TUI report panels + palette polish.

  • Report panels: completed metadata_diff / validate_site tasks open a scrollable panel (↑/↓) showing the differences — per-entity field diffs (db=… → file=…) plus orphan files, and missing/extra/updated path sections plus sitemap status for validation. enter applies the whole report: metadata diff repairs all items from the filesystem (file → db) and imports all orphans; site validation applies the full report incrementally, same as the GUI. esc cancels without applying. Reports completed before the session started never pop up (pre-existing completed tasks are marked handled at mount).
  • Palette fixes: overlays (palette, reports, image preview) now render a Clear widget underneath — no more background text shining through. ? in the sidebar is gone; help is :? from inside the prompt, vscode-palette-style type-to-filter was already in place (matches command ids and localized labels).
  • Since both reports are now fully actionable in the terminal, their [report/apply in GUI] markers were dropped; only validate_translations and find_duplicates keep it.
  • 9 new/updated tests (report open/scroll/apply/cancel, pre-mount task suppression, background clearing, :? help); spec/README/all four locales updated. Full suite 1362/0, credo/deps.audit/dialyzer clean.
**Post-phase addition** (commit 39bb232): TUI report panels + palette polish. - **Report panels:** completed `metadata_diff` / `validate_site` tasks open a scrollable panel (`↑/↓`) showing the differences — per-entity field diffs (`db=… → file=…`) plus orphan files, and missing/extra/updated path sections plus sitemap status for validation. `enter` applies the whole report: metadata diff repairs **all** items from the filesystem (file → db) and imports all orphans; site validation applies the full report incrementally, same as the GUI. `esc` cancels without applying. Reports completed before the session started never pop up (pre-existing completed tasks are marked handled at mount). - **Palette fixes:** overlays (palette, reports, image preview) now render a `Clear` widget underneath — no more background text shining through. `?` in the sidebar is gone; help is `:?` from inside the prompt, vscode-palette-style type-to-filter was already in place (matches command ids and localized labels). - Since both reports are now fully actionable in the terminal, their `[report/apply in GUI]` markers were dropped; only `validate_translations` and `find_duplicates` keep it. - 9 new/updated tests (report open/scroll/apply/cancel, pre-mount task suppression, background clearing, `:?` help); spec/README/all four locales updated. Full suite 1362/0, credo/deps.audit/dialyzer clean.
Author
Owner

Post-phase fix (commit a8ca3b6): typing a filter that matches no command (e.g. :j) crashed the TUI render on every frame — ExRatatui's List widget raises when selected is set on an empty collection, which painted errors across the screen. All three dynamic lists (command palette, sidebar, report panel) now clamp through a shared list_selected/2 helper that returns nil for empty collections and clamps out-of-range indexes; the sidebar had the same latent bug for empty views. Reproduced by a new regression test before fixing. Full suite 1363/0, credo/dialyzer clean.

**Post-phase fix** (commit a8ca3b6): typing a filter that matches no command (e.g. `:j`) crashed the TUI render on every frame — ExRatatui's `List` widget raises when `selected` is set on an empty collection, which painted errors across the screen. All three dynamic lists (command palette, sidebar, report panel) now clamp through a shared `list_selected/2` helper that returns `nil` for empty collections and clamps out-of-range indexes; the sidebar had the same latent bug for empty views. Reproduced by a new regression test before fixing. Full suite 1363/0, credo/dialyzer clean.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: hugo/bDS2#26