# TUI design guide This guide is the normative design contract for the `hosts` terminal user interface. It evolves the application's existing dark, keyboard-first, master-detail interface into a consistent system without replacing its visual identity. The words **must**, **should**, and **may** distinguish requirements, recommendations, and optional choices. The guide describes the target state; current deviations are implementation work, not exceptions or user-reachable features. User-facing documentation must continue to describe only behavior that the application currently provides. ## Design character `hosts` is a calm, trustworthy operations console. It should feel dense and capable without feeling busy, playful, or alarming during ordinary use. Every screen follows these principles: 1. **Information before decoration.** Structure, alignment, and concise text do more work than borders, color, or ornament. 2. **Safety remains visible.** Mode, protection, failures, and consequences are explicit and persist for as long as they matter. 3. **The keyboard path is complete.** Every workflow works without a mouse; natural mouse support remains available where Textual provides it. 4. **State has more than one signal.** Color may reinforce a state but never carries essential meaning alone. 5. **The terminal is user-owned.** The interface does not assume a particular font, truecolor renderer, glyph width outside predictable one-cell symbols, or a large viewport. ## Viewports and composition The interface has three viewport modes: | Terminal size | Contract | | --- | --- | | At least 120 columns by 40 rows | Design target. Show the full 60/40 master-detail workspace. | | At least 100 columns by 30 rows | Minimum usable size. Keep the 60/40 workspace, compact the details, and abbreviate contextual controls without hiding required actions. | | Below 100 columns or below 30 rows | Replace the workspace with a minimum-size presentation. Do not silently clip, overlap, or squeeze controls into unusable shapes. | The target layout is: ```text ┌──────────────────────────── hosts ─────────────────────────────┐ │ message rail: transient result or persistent problem │ ├─ Search ────────────────────────────────────────────────────────┤ │ query Filters: 2 │ ├─ Host Entries (60%) ───────────────┬─ Entry Details (40%) ─────┤ │ state IP address hostname DNS │ IP address value │ │ ... │ Hostnames value │ │ │ DNS details value │ │ │ Comment — │ ├────────────────────────────────────┴────────────────────────────┤ │ contextual keys count · filters · READ-ONLY │ └─────────────────────────────────────────────────────────────────┘ ``` At the minimum usable size, preserve the same regions but use compact label-value rows in Entry Details and shorter footer descriptions. If either dimension falls below the minimum, render a stable presentation such as: ```text ┌──────────────────── hosts ────────────────────┐ │ Terminal too small │ │ │ │ hosts requires at least 100 columns × 30 rows │ │ Current size: 86 × 24 │ │ │ │ q Quit │ └───────────────────────────────────────────────┘ ``` The minimum-size presentation must not permit a hidden or ambiguous mutation. It may retain safe global actions such as Quit. ### Regions - The header identifies the application and exposes safety-critical mode when Privileged Mode is active. - The message rail occupies one reserved row below the header. It never covers the workspace. - Search remains one row above the master-detail workspace. - Host Entries and Entry Details use an approximate 60/40 split. Their minimum content widths take priority over an exact percentage. - The footer shows contextual keys on the left and durable state on the right. It must not rely on horizontal scrolling. ## Theme and color The application owns a semantic `hosts-dark` theme rather than inheriting unspecified library defaults. Its initial palette preserves the current Textual-dark appearance: | Token | Initial dark value | Purpose | | --- | --- | --- | | `background` | `#121212` | Screen background | | `surface` | `#1E1E1E` | Inputs and alternating table rows | | `panel` | `#242F38` | Header, footer, and elevated containers | | `foreground` | `#E0E0E0` | Primary text | | `primary` | `#0178D4` | Structure, focus, and selection | | `secondary` | `#004578` | Secondary structure | | `accent` | `#FFA62B` | Key hints and exceptional emphasis | | `success` | `#4EBF71` | Successful outcomes and confirmed healthy state | | `warning` | `#FFA62B` | Caution, inactivity, and Privileged Mode emphasis | | `error` | `#BA3C5B` | Failures and destructive actions | Components must consume semantic tokens such as `$primary`, `$success`, and `$text-muted`; they must not embed named Rich colors such as `green` or `yellow`. Accent and warning happen to share an initial value but remain separate roles. Dark is the default appearance. A future light theme may choose different values, but it must preserve the same semantic roles, hierarchy, and state redundancy. Text should approximate a contrast ratio of at least 4.5:1 where the renderer permits controlled color. Focus and meaningful large text should approximate at least 3:1 against adjacent colors. Use chromatic color sparingly: - Primary blue describes structure, focus, and selection. - Green describes a successful result or active mapping, never a generic decoration. - Amber describes caution, inactivity, or elevated privilege. - Red describes failure or destructive consequence. - Muted foreground describes secondary information, not information required to finish a workflow. ## Text, symbols, and density The terminal controls the font. Assume a legible monospace font and make no layout or branding decision that depends on a particular typeface. - Use bold for headings, keys, and the strongest current emphasis. - Use dim text for supporting information. - Use italics only as reinforcement; some terminals do not distinguish them. - Use sentence case for headings, labels, buttons, and messages. - Use one-cell spacing increments. - Use **compact** density for tables, details, footers, and status areas. - Use **comfortable** density for forms and modals so fields remain easy to navigate and errors have room to appear. Predictable one-cell Unicode symbols such as `✓`, `×`, and `!` may reinforce a state. Emoji are not structural UI: their width, color, and availability vary too widely. Essential meaning must survive if a symbol is absent or rendered poorly. Details, help, and messages therefore use words in addition to any marker. ## Borders and hierarchy Rounded single-line borders define major regions: Search, Host Entries, and Entry Details. Nested borders should not make every value compete with its parent region. Within a region, prefer alignment, whitespace, text weight, and subtle surface changes. Inputs receive an input treatment only while they are editable. Read-only values are label-value text, not disabled inputs. Thick borders are reserved for modals; the error color is reserved for destructive confirmation and failure states. Good: ```text ╭─ Entry Details ─────────────────────╮ │ IP address 192.0.2.10 │ │ Hostnames api.local, api │ │ Comment — │ │ State ✓ Active │ ╰─────────────────────────────────────╯ ``` Avoid: ```text ╭─ Entry Details ─────────────────────╮ │ ╭─ IP Address ────────────────────╮ │ │ │ disabled input │ │ │ ╰────────────────────────────────╯ │ │ ╭─ Comment ───────────────────────╮ │ │ │ disabled input │ │ │ ╰────────────────────────────────╯ │ ╰─────────────────────────────────────╯ ``` ## State grammar Each interaction state has a distinct visual channel: | State | Required treatment | | --- | --- | | Focus | Bright primary border or cursor that remains visible independently of selection | | Selection | Contrasting row background; moving focus away must not erase which Host Entry is selected | | Active Entry | Stable marker plus normal or success-toned text | | Inactive Entry | Different marker plus warning-toned or muted text; italics may reinforce it | | Default Entry | Explicit protected marker plus muted treatment and an explanation in Entry Details | | Disabled action | Muted but still visible when hiding it would make a restriction mysterious | | Work in progress | Text such as `Resolving…` plus an optional one-cell activity marker | | Failure | Error token plus an actionable textual message | | Privileged Mode | Persistent `PRIVILEGED` wording plus restrained warning emphasis | Selection, focus, activity, and protection are independent. A selected Inactive Entry must still look selected; a focused Default Entry must still look protected. ## Core components ### Search and filters The top Search field performs immediate text filtering. `Ctrl+F` opens advanced criteria. Both surfaces edit one coherent filter state and must show the same search term. When non-text criteria are active, the workspace exposes a compact indicator, such as `Filters: 2`. A no-results state says that filters matched no Host Entries and presents the shortcut for changing or clearing them. Placeholders may provide examples, but never replace a persistent label when a field's meaning would otherwise disappear after typing. ### Host Entries table The table is a scan surface, not a compressed details view. Preserve columns under horizontal pressure in this order: 1. state; 2. IP address; 3. Canonical Hostname; 4. DNS status. Never truncate an IP address. Long hostnames may end in an ellipsis; Entry Details shows their complete values. Column widths remain stable while the selection moves. DNS status uses a short, predictable marker in the table and complete wording in Entry Details. ### Entry Details Display inspection data as compact aligned label-value rows. Ordinary IP-address Host Entries do not show empty DNS Name, DNS Status, or Last Resolved rows. DNS Entry fields appear only when they apply. Use `—` for an absent optional value such as a comment. When no Host Entry is selected, say so rather than showing a field-shaped empty screen. When no entries exist or no filter results match, name the condition and offer the relevant next action. ### Entry Editor and forms Forms use persistent labels, consistent required-field markers, and a visual order that matches their Tab order. Validation appears directly beneath the responsible field, retains the user's input, and explains how to correct the value. The primary action is visually prominent and last in keyboard order. Cancel is always available and non-destructive. Arrow keys operate grouped controls such as radio buttons; Tab and Shift+Tab move between fields and action groups. ### Modals Modals are centered, bounded by the viewport, and share this structure: 1. concise title; 2. explanation or form content; 3. inline validation or consequence text; 4. action row. Neutral and complex modals use the primary border. Only destructive confirmation uses the error border. `Escape` cancels without mutation. Initial focus lands on the safest sensible action; destructive confirmation never defaults to the destructive button. ### Footer and help The footer is contextual. It shows the small set of actions most relevant to the current state rather than the complete binding catalog. Descriptions may shorten in compact mode, but the keys and durable state remain visible. The right side reports entry count, active filters, and either `READ-ONLY` or `PRIVILEGED`. `?` opens a dedicated overlay or screen instead of docking a panel that shrinks the workspace. Group help under General, Navigation, Filtering, and Privileged Mode. `?` and `Escape` close it and restore previous focus. ## Feedback and safety The reserved message rail reports action results without covering content or moving the master-detail workspace. - Routine success and informational messages may expire. - Errors, warnings, and unresolved safety conditions persist until the user acknowledges them or a later result supersedes them. - Durable state such as mode and active filters belongs in the footer. - An error says what happened, whether state changed, and what the user can do next. Use the canonical terms from `CONTEXT.md`: **Read-only Mode**, **Privileged Mode**, and **Entry Editor**. `Edit Mode` must not name Privileged Mode. Entering Privileged Mode does not recolor the entire interface. A persistent, warning-toned `PRIVILEGED` indicator communicates elevated capability without normalizing alarm colors. Mutating actions become available contextually. Default Entries have a visible protected marker and an explanation in Entry Details. When a protected mutation is unavailable, keep the action visible if hiding it would make the restriction unclear, and explain the restriction in the message rail or help. Confirm destructive operations and attempts to leave an Entry Editor with unsaved changes. Name the affected Host Entry and initially focus the safest sensible action. Routine navigation and inspection do not require confirmation. DNS resolution and other asynchronous work keep the interface responsive. Show `Resolving…` inline for the affected DNS Entry. Completion or failure must not move selection or steal focus unexpectedly. Motion is optional, brief, and nonessential. It must not delay input, hide state, or be necessary to understand an outcome. ## Input and focus Every workflow must be keyboard-complete. Mouse support should work where Textual provides it naturally, but no action may depend on hover or a pointer. - Tab order follows visual reading order. - Arrow keys operate tables and controls that conventionally use them. - `Escape` backs out without mutation or opens the applicable unsaved-change confirmation. - Closing a modal or help restores focus to the control that opened it. - Application shortcuts must not fire while a user is typing into a field unless the shortcut is a conventional editing or escape command. - Focus remains visible in dark, light, 256-color, and degraded-color output. ## Writing English is the canonical interface language. Use concise, direct verbs and the domain language in `CONTEXT.md`. Write complete strings and avoid layout assumptions that make later localization needlessly difficult; an internationalization architecture is outside the current contract. Good and avoid: | Good | Avoid | Reason | | --- | --- | --- | | `Enter Privileged Mode` | `Toggle edit mode` | Names the resulting safety state | | `Delete Host Entry` | `Delete Entry!` | Uses the canonical concept without decorative alarm | | `Save failed; previous state restored. Check write access and try again.` | `❌ Save error` | States outcome, data safety, and recovery | | `No Host Entries match the current filters.` | `Nothing here` | Names the condition | | `—` | `N/A` | Represents an absent optional value without implying an error | ## Accessibility and terminal compatibility The supported visual baseline is a common 256-color terminal. Truecolor may improve fidelity. Monochrome or degraded-color output may lose polish but must retain all essential meaning through wording, markers, position, and text style. Do not depend on: - color as the only state signal; - italics, dim text, or animation as the only distinction; - emoji or ambiguous-width glyphs; - a particular terminal font; - mouse hover; - content hidden outside the supported viewport without a visible way to reach it. ## Verification Changes to the TUI must preserve behavior and essential visibility at 120×40 and 100×30. Tests should use Textual's test harness to cover: - keyboard-complete workflows; - visual-order focus traversal and focus restoration; - selection remaining distinct from focus and entry state; - mode, protection, validation, progress, and failure wording; - conditional detail fields and empty states; - absence of clipped or inaccessible essential controls at supported sizes. Test structure and semantics rather than terminal pixels or exact glyph rendering. Significant visual changes also receive an `agent-tui` inspection at both supported sizes. Screenshots are illustrative evidence, not golden output. Before review, check: - Does the change preserve the calm operations-console character? - Are all states legible without color? - Is the keyboard path complete and ordered like the screen? - Are canonical Hosts Management terms used? - Does feedback remain visible for as long as it matters? - Are 120×40 and 100×30 both usable without essential clipping? - Does the full key reference remain reachable through `?`? ## Evolving the guide This contract applies to the whole TUI, including existing screens. Track current deviations in Forgejo rather than weakening the rules with permanent exceptions. An intentional departure must be explained in the relevant change. Update this guide when the departure should become the new standard. Add an ADR only when the decision is consequential, surprising, difficult to reverse, and the result of a real trade-off.