-- allium: 1 -- Terminal UI (issue #26, phase 4) -- Scope: extension (second renderer over the shared UI core) -- Distilled from: lib/bds/tui.ex, lib/bds/ui/sidebar.ex, lib/bds/ui/post_editor/*.ex entity TuiState { view: posts | media | templates | scripts | tags | settings | git focus: sidebar | editor selected_index: Integer editing_post: Boolean -- The editor drives the same BDS.UI.PostEditor workflow as the GUI: -- canonical-language edits update the post, other languages update -- translations; publish routes through the same publishing pipeline. } surface TuiSurface { facing _: TuiRuntime provides: TuiKeyPressed(code, modifiers) TuiEventReceived(entity, entity_id, action) TuiSettingsChanged(key) ShellTaskCompleted(route) } rule SidebarNavigation { when: TuiKeyPressed(code: "up" | "down" | "j" | "k") -- Selection moves over the flattened sidebar items and skips -- section headers; data comes from BDS.UI.Sidebar.view, identical -- to the GUI sidebar content. ensures: TuiState.selected_index.updated() } rule OpenEntry { when: TuiKeyPressed(code: "enter") -- Posts open in the editor by default (title + textarea seeded from -- the persisted form; syntax highlighting and word wrap on; ctrl+e -- switches to the rendered preview). New empty posts created with -- "n" open in the editor as well. Images open in the terminal image -- preview. Other entities report that terminal editing is not -- available yet. ensures: TuiState.editing_post.updated() } rule SaveAndPublish { when: TuiKeyPressed(code: "s" | "p", modifiers: "ctrl") -- ctrl+s saves the draft, ctrl+p saves and publishes — both through -- BDS.UI.PostEditor.Persistence, so files, embeddings, search, and -- the event bus behave exactly as a GUI save. ensures: PostPersisted() } rule EditorMode { when: TuiKeyPressed(code: "e", modifiers: "ctrl") -- ctrl+e toggles between the syntax-highlighted editor (the default -- mode when opening a post) and the read-only rendered-Markdown -- preview (rendered through tui-markdown so headings/emphasis/lists -- are styled); keys in preview never edit content, and esc returns -- to the sidebar from either mode. ensures: PreviewToggled() } rule CodeEditorWrapping { when: TuiState.editing_post.updated() -- The post editor renders the buffer with syntax highlighting -- (markdown with [[macro]] accents for posts, HTML+Liquid for -- templates, Lua for scripts), soft-wraps long lines to the -- viewport width, keeps the cursor's visual row visible while -- scrolling, and highlights the cursor's source line. There is no -- line-number gutter. ensures: TuiState.updated() } rule AiQuickAction { when: TuiKeyPressed(code: "g", modifiers: "ctrl") -- One-shot AI post analysis (title/excerpt suggestions). Gated by -- airplane mode: without a local endpoint the TUI shows a status -- message instead of calling out. ensures: AiSuggestionsRequestedOrRefused() } rule ProjectSwitcher { when: TuiKeyPressed(code: "p") -- "p" opens the projects overlay: all projects from the caching -- database with the active one marked; enter activates the selected -- project (BDS.Projects.set_active_project) and reloads the sidebar. -- "o" switches to a folder-path prompt with bash-style tab -- completion: tab completes a unique directory (trailing slash -- appended), an ambiguous prefix completes to the longest common -- prefix and lists the candidate folders; only directories are -- offered, dotfolders only for a dot-prefixed input, and a leading -- "~" expands to the home directory. A typed path is validated -- to be an existing directory, then opened as a project -- (BDS.Projects.create_project with data_path, named after the -- folder) and activated; a full database rebuild is queued -- automatically through the rebuild_database shell command so the -- folder's content is loaded into the caching database. Invalid -- paths keep the prompt open and report the error. ensures: ProjectSwitchedOrOpened() } rule SidebarSearch { when: TuiKeyPressed(code: "/") -- "/" in sidebar focus opens a vi-style search prompt in the status -- line that live-filters the current view through the same -- BDS.UI.Sidebar filter params the GUI sidebar uses (posts, pages, -- media). Plain words are a text search, "tag:x" and "category:x" -- filter like the GUI chips, and an ISO date (yyyy-mm-dd) narrows -- to that day — the day filter extends the shared sidebar queries. -- Tokens combine with AND. Enter keeps the filter (shown appended -- to the sidebar title), esc clears it; filters are per view. ensures: SidebarFiltered() } rule CommandPrompt { when: TuiKeyPressed(code: ":") -- Vi-style prompt: ":" opens a filterable list of the parameterless -- Blog-menu shell commands (metadata diff, validate site, force -- render, rebuilds, reindex, translations, duplicates, upload, -- browser preview URL) and runs the selection through the same -- BDS.Desktop.ShellCommands backend as the GUI menu; typing ":?" -- shows the full list as help. Overlays clear the cells beneath -- them. Commands whose follow-up UI is GUI-only (validate -- translations, find duplicates) are marked. Queued tasks report -- progress in the status line until all tasks finish. ensures: CommandListShownOrExecuted() } rule SettingsPanel { when: TuiKeyPressed(code: "6") -- "6" opens the settings panel (issue #29), matching the GUI panel -- numbering: the sidebar lists the same preference sections as the -- settings backends (Project, Editor, Content, AI, Technology, -- Publishing, Data, MCP). The GUI-only Style tab is intentionally -- absent. Enter on a section opens a section-specific editor in the -- main area: a generic typed-field form from BDS.UI.SettingsForm. -- Enter edits a text field in a status-line prompt, toggles a -- boolean, or cycles an enum through its options; read-only info -- rows are skipped by the selection. ctrl+s saves the section -- through the same backends as the GUI editor (BDS.Metadata, -- BDS.Settings, BDS.AI, BDS.MCP.AgentConfig) and reloads the form; -- esc cancels the prompt, then closes the form. Category removal -- and AI model discovery remain GUI-only. ensures: SettingsFormShownEditedOrSaved() } rule TagsPanel { when: TuiKeyPressed(code: "5") -- "5" opens the tags view (issue #34), matching the GUI panel -- numbering. The sidebar lists the same three sections as the GUI -- tags editor (Tag Cloud, Create / Edit, Merge Tags — from -- BDS.UI.Sidebar's tags nav view); enter opens the section in the -- main area, all backed by BDS.UI.TagsPanel over the same BDS.Tags -- operations as the GUI editor. Cloud lists tags with their post -- usage counts ordered by usage; manage is alphabetical with "n" -- (create prompt), enter (rename prompt), "c" (cycle colour through -- the shared presets), "t" (cycle post template), "d" then "y" -- (delete with post-count confirmation; any other key cancels) and -- "s" (sync tags from post tags); merge marks tags with space and -- "m" merges the marked tags into the selected one (at least two, -- target included). Text prompts live in the status line; esc -- cancels the prompt, then closes the panel. Domain tag events -- reload an open panel, keeping it in sync with GUI and CLI writes. ensures: TagsPanelShownOrEdited() } rule GitPanel { when: TuiKeyPressed(code: "7") -- "7" opens the git panel (issue #30) for content sync: the sidebar -- lists the changed files from git status (code + path, branch with -- ahead/behind counts in the title) with the commit history in its -- lower half, like the GUI (short hash + subject; ↑ marks commits -- that still need a push, ↓ remote-only ones), the main area shows a -- scrollable whole-folder diff (staged + unstaged, capped) paged -- with pgup/pgdn; enter on a file jumps the diff to it. "c" opens a -- status-line commit prompt (git add -A + commit, empty messages -- rejected), "u" pulls (ff-only) and "s" pushes — both run off the -- UI process and report back as a status toast. Every action is -- BDS.Git, the same backend as the GUI git panel. A project folder -- that is not a git repository shows a message and refuses the -- actions. ensures: GitPanelShownOrSynced() } rule ReportPanels { when: ShellTaskCompleted(route: "metadata_diff" | "site_validation") -- Completed metadata diff and site validation tasks open a -- scrollable report panel showing the differences. Enter applies -- the whole report — metadata diff repairs all items from the -- filesystem (file → db) and imports orphan files; site validation -- applies the full report incrementally (render missing, remove -- extra, re-render updated), matching the GUI defaults. Esc closes -- the report without applying. Reports completed before this -- session started never pop up. ensures: ReportShownThenAppliedOrCancelled() } rule MultiClientSync { when: TuiEventReceived(entity, entity_id, action) -- Domain events refresh the sidebar; a post deleted elsewhere closes -- the open editor. Keeps TUI sessions synchronized with GUI clients -- and pipeline ingestion. ensures: TuiState.updated() } rule ServerSideLocale { when: TuiSettingsChanged(key: "ui.language") -- The TUI re-reads the server-side UI language and re-renders; all -- clients always share one language. ensures: TuiRelocalized() }