Files
signer/plans/tui_flow_redesign.md
T

592 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Plan: TUI Flow Redesign
## Goal
Simplify the signer TUI from a multi-screen setup wizard into a single
main screen with full-screen overlays. The seed phrase is entered once
at startup in a popup; all subsequent editing (roles, transport, lock)
is done from full-screen overlays reached from the main screen. Borders
are collapsed for a cleaner look. A dedicated Commands screen provides
keyboard navigation through all available actions with a visible
cursor. Command hints show only the word with the key letter
underlined (e.g. "Quit" with Q underlined, not "Q quit").
## Current state
- [`src/tui.rs`](../src/tui.rs:1) — 1398 lines. Seven screens:
`Unlock`, `GenerateMnemonic`, `RoleWizard`, `TransportSelection`,
`Main`, `Connections`, `Lock`. Setup is a linear wizard: Unlock →
(Generate) → RoleWizard → TransportSelection → Main. The main screen
has a title bar reading `signer v0.0.1 > Main Menu`, a two-column
body (Information + Roles on the left, Activity on the right), and a
Commands bar at the bottom. Connections and Lock are rendered as
sub-panels inside the main screen's right column.
- [`src/main.rs`](../src/main.rs:129) — `server_main` constructs `App`
and calls `run()`. Non-interactive mode uses `run_headless()`.
- [`src/server.rs`](../src/server.rs:80) — `ServerContext` supports
`Unix`, `Qrexec`, `Tcp`, `Http`, `Stdio` listen modes. Only one mode
active at a time (the `start_server` method picks the first toggled
transport).
## Key design decisions
### 1. Rename "client name" → "signer name"
The user asked whether "client name" should be renamed since the
signer is more a server than a client. **Decision: rename to "signer
name".** The field shows the socket name (e.g. `nsigner01`), which is
the name clients use to connect. Calling it "signer name" is clearer
than "client name" and consistent with the program name.
### 2. Single main screen, no setup wizard
The current linear wizard (Unlock → RoleWizard → TransportSelection →
Main) is replaced by:
1. **Startup popup** — seed phrase entry (and optional generation).
This is the *only* popup in the entire TUI.
2. **Main screen** — shows Information, Roles, Activity, and a Commands
bar. From here the user can:
- Open the Roles screen (add/remove roles) — full-screen overlay
- Open the Transport screen (toggle transports on/off at any time) —
full-screen overlay
- Lock the session — full-screen overlay
- Open the Commands screen — full-screen overlay
- Quit
### 3. Collapsed borders
Use ratatui's `MergeStrategy::Exact` with `Spacing::Overlap(1)` so
adjacent blocks share borders instead of drawing double lines. The
selected/focused pane gets a thick border for visual distinction. See
[`ratatui/ratatui-widgets/examples/collapsed-borders.rs`](../ratatui/ratatui-widgets/examples/collapsed-borders.rs:1)
for the reference implementation.
### 4. Command hint style
Command hints at the bottom of each screen show only the word with the
key command letter underlined — not "Q quit" but "Quit" with the Q
underlined. The underlined letter is the actual key binding, which may
not be the first letter. For example, if `b` is the key for "Qube
bridge", it would be rendered as "Qube bridge" with the `b`
underlined. This is achieved with
`Span::raw("Qube ")` + `Span::styled("b", Style::default().add_modifier(Modifier::UNDERLINED))`
+ `Span::raw("ridge")`.
Each command hint is a word (or short phrase) with exactly one letter
underlined — the letter the user presses to activate that command.
## Target layout
### Startup popup — seed phrase entry
A centered popup (not full screen) over a blank terminal. This is the
only popup in the entire TUI:
```
┌──────────────────────────────────────────────┐
│ Signer v0.0.1 │
│ │
│ Enter seed phrase or G to generate new: │
│ > abandon abandon abandon abandon abandon │
│ abandon abandon abandon abandon abandon │
│ abandon abandon │
│ │
│ Invalid mnemonic. Attempts: 1/10 │
└──────────────────────────────────────────────┘
```
- If the user types `g` and presses Enter, generate a 12-word
mnemonic, display it in the same popup, then press Enter to
continue.
- On successful load, derive keys for any pre-registered roles (the
default `main` role is auto-registered), start the server with
default transport (Unix), and transition to the main screen.
- Invalid mnemonic shows an error line and retries (max 10 attempts).
### Main screen — full mockup
Title is `Signer v0.0.1` centered on its own line — no border box
around it, no "Main Menu" text. Use ratatui's `Line::from(...).centered()`
to center the title. Collapsed borders between the body sections.
Left column has three sections: Information (top), Transport (middle),
Roles (bottom). Right column has Activity (scrollable, newest first).
Commands bar at the bottom with key command letters underlined.
Below is the full main screen showing all sections in detail. The
`▸` cursor in the Transport section shows the currently selected
transport line. Active transports are shown in **bold** (rendered as
reversed video or bold in the actual TUI). The Activity column shows
the C-format log entries (newest first).
```
Signer v0.0.1
├──────────────────────────────────────────┬──────────────────────────────────────────┤
│ Information │ Activity │
│ │ │
│ signer name: nsigner01 │ 2026-08-18 15:05:42 unix:1000 │
│ Unix address: │ sign_event(main) ALLOWED:no-auth │
│ nsigner01 │ 2026-08-18 15:05:30 unix:1000 │
│ Qube address: │ get_info() ALLOWED:no-auth │
│ (inactive) │ 2026-08-18 15:04:55 unix:1000 │
│ FIPS address: │ nip44_encrypt(main) ALLOWED:no-auth │
│ (inactive) │ │
│ HTTP address: │ │
│ (inactive) │ │
│ OTP pad: chksum=a1b2c3 offset=128/4096 │ │
│ │ nip44_encrypt(main) ALLOWED:no-auth │
├──────────────────────────────────────────┤ 2026-08-18 15:03:12 unix:1000 │
│ Transport │ sign_event(nostr_range,0) │
│ │ ALLOWED:no-auth │
│ ▸ [x] U̲nix Socket │ 2026-08-18 15:02:00 unix:1000 │
│ [ ] Qube b̲ridge │ get_info() ALLOWED:no-auth │
│ [ ] F̲IPS │ 2026-08-18 15:01:30 unix:1000 │
│ [ ] H̲TTP │ sign_event(main) ALLOWED:no-auth │
│ │ 2026-08-18 15:00:22 unix:1000 │
├──────────────────────────────────────────┤ sign_event(main) ALLOWED:no-auth │
│ Roles │ 2026-08-18 15:00:10 nsigner started │
│ │ │
│ Role Purpose Curve │ │
│ ───────────── ──────── ──────────── │ │
│ main nostr secp256k1 │ │
│ nostr_range nostr secp256k1 │ │
│ ssh ssh ed25519 │ │
│ │ │
│ A̲dd D̲elete │ Cl̲ear ▲ │
├──────────────────────────────────────────┴──────────────────────────────────────────┤
│ He̲lp Q̲uit │
└─────────────────────────────────────────────────────────────────────────────────────┘
```
Each section has its own commands on the bottom line, left-aligned:
- **Information**: no commands (display only)
- **Transport**: no separate command — each transport line is a toggle
button. The `[x]` / `[ ]` indicator shows on/off state. The key
command letter is underlined in each label (`U̲nix Socket`,
`Q̲ube bridge`, `F̲IPS`, `H̲TTP`). Tab or Up/Down moves between
lines, Enter or the underlined key toggles that transport on/off
(radio-button: one active at a time, server restarts immediately).
Active transport is also shown in bold/reversed.
- **Roles**: `A̲dd D̲elete` — add a new role, delete the selected role
- **Activity**: `Cl̲ear` — clear the activity log (with a blank row
above the command)
- **Bottom bar**: `He̲lp Q̲uit` — global navigation (Help opens a
help screen, Quit exits)
**Section details:**
**Information** — each transport address is shown as a label row
followed by an indented value row (since addresses can be long):
| Label row | Indented value row | Source |
|-----------|-------------------|--------|
| `signer name: <socket_name>` | (same line) | `self.socket_name` |
| `Unix address:` | ` <socket_name>` (without @) | if Unix active, else ` (inactive)` |
| `Qube address:` | ` (one request per invocation)` | if Qrexec active, else ` (inactive)` |
| `FIPS address:` | ` <bind_addr>` | if TCP active, else ` (inactive)` |
| `HTTP address:` | ` <bind_addr>` | if HTTP active, else ` (inactive)` |
| `OTP pad: chksum=… offset=N/M` | (same line) | `otp_pad::global_status()` (only if bound) |
Note: Unix addresses are always displayed without the `@` symbol.
The indented value row uses 2-space indentation.
**Transport** — 4 toggle-button lines, each showing `[x]` or `[ ]`
indicator plus the transport name with the key letter underlined
(`U̲nix Socket`, `Qube b̲ridge`, `F̲IPS`, `H̲TTP`). No separate
command line — each line is its own toggle. Tab or Up/Down moves
between lines, Enter or the underlined key letter toggles that
transport on/off (radio-button: one active at a time, server
restarts immediately). Active transport also shown in bold/reversed.
Key assignments (all unique across the main screen — this is the
canonical set shown in the mockup):
- `U` — Unix Socket
- `B` — Qube bridge
- `F` — FIPS
- `H` — HTTP
- `A` — Add role
- `D` — Delete role
- `C` — Clear activity log
- `L` — Help screen
- `Q` — Quit
**Roles** — table with columns: Role, Purpose, Curve. (Derivation path
is omitted from the main screen to save space — it's visible on the
Roles overlay screen.) Shows all registered roles. A blank row
separates the table from the commands at the bottom: `A̲dd D̲elete`.
**Activity** — scrollable, newest first. Each entry is a timestamped
log line in the C format (see "Activity log format" below). Scrollbar
on the right. A blank row separates the log from the commands at the
bottom: `Cl̲ear`.
**Bottom bar** — `He̲lp Q̲uit` with key letters underlined.
### Activity log format
The current Rust implementation logs only "request handled" — it does
not show what was actually handled. The C implementation logs detailed
request information. Copy the C format:
```
<caller_id> <method>(<role_name>[,<concrete_path>]) <verdict>:<source_label>
```
Examples:
- `unix:1000 sign_event(main) ALLOWED:no-auth`
- `unix:1000 sign_event(nostr_range,0) ALLOWED:no-auth`
- `unix:1000 get_info() ALLOWED:no-auth`
- `tcp:[::1]:12345 nip44_encrypt(main) ALLOWED:no-auth`
**Implementation:** `ServerContext::process_request` must return an
activity description string alongside the JSON response. Change the
return type to `(String, String)` — the response and the activity log
message. The activity message is constructed from:
- `caller.caller_id` — e.g. `unix:1000` or `tcp:[::1]:12345`
- `method` — the JSON-RPC method (e.g. `sign_event`, `get_info`)
- `role_name` — from the resolved selector (if a role was matched)
- `concrete_path` — if the role has a path template with `%d`, the
concrete index (e.g. `0`)
- `verdict` — `ALLOWED` (since we removed policy, all valid requests
are allowed; denied requests get `DENIED` with the error reason)
- `source_label` — `no-auth` (since we removed policy/authorization)
The `service_server()` method in `App` passes this message to
`activity_log.add()` instead of the generic "request handled".
### Roles section (on main screen)
Roles are managed directly in the Roles section on the main screen —
there is no separate Roles overlay screen. The `▸` cursor shows the
selected role.
- **Add** (`A`): opens the `AddRole` popup showing the role preset
menu (same 1–10 presets as current wizard). Select a preset (or
custom), then enter role name and path template via `InputField`
with pre-filled defaults. On confirm, register the role and derive
its key immediately, then return to the main screen.
- **Delete** (`D`): deletes the currently selected role immediately —
no confirmation overlay. The role is removed from the table and its
derived key is wiped. The selection moves to the next role.
- **Select**: Up/Down arrows or Tab move selection through the role
list. The `▸` cursor shows the selected role.
### Help screen
Opened by pressing `L` from the main screen. Full-screen overlay that
describes what the app does, what transports are, what roles are, and
lists the key commands at the end. It is a scrollable screen (the
content can exceed the visible height). Commands at the bottom:
```
Signer v0.0.1
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ │
│ Signer is an attended Nostr signing daemon. It holds your keys and signs │
│ requests from clients over one or more transports. │
│ │
│ Transports │
│ ────────── │
│ A transport is a way for clients to reach the signer. │
│ - Unix Socket: local same-machine access via an abstract socket. │
│ - Qube bridge: access from other Qubes via qrexec. │
│ - FIPS: TCP listener for framed JSON over a network. │
│ - HTTP: HTTP listener for curl-friendly requests. │
│ │
│ Roles │
│ ───── │
│ A role binds a name to a derivation path and curve. Clients address │
│ requests by role name, which also serves as the password. │
│ │
│ Key commands │
│ ───────────── │
│ U Toggle Unix Socket transport │
│ B Toggle Qube bridge transport │
│ F Toggle FIPS transport │
│ H Toggle HTTP transport │
│ A Add a role │
│ D Delete the selected role │
│ C Clear the activity log │
│ L Open the Help screen │
│ Q Quit │
│ │
├──────────────────────────────────────────────────────────────────────────────────────┤
│ B̲ack │
└──────────────────────────────────────────────────────────────────────────────────────┘
```
- The content is scrollable — Up/Down arrows (or Page Up/Down) scroll
through the help text. A scrollbar is shown on the right when the
content overflows.
- ESC / `B` (Back): return to main screen.
- This is a reference screen — no actions are executed from here.
## Screen enum (updated)
```rust
pub enum Screen {
/// Startup popup — seed phrase entry
SeedEntry,
/// Startup popup — showing generated mnemonic
SeedDisplay,
/// Main status screen (Information + Transport + Roles + Activity + bottom bar)
Main,
/// Add-role popup — role preset selection (over the Main screen)
AddRole,
/// Help — full-screen overlay describing the app and key commands
Help,
}
```
`Connections`, `Transport`, `Lock`, and `Commands` screens are removed.
Transport is now a section on the main screen (between Information and
Roles). Connection info is shown inline in the Information section
(transport addresses). Lock functionality is removed entirely. The
Commands screen is replaced by a Help screen. Only `SeedEntry` and
`SeedDisplay` are popups; all other screens are full-screen overlays.
## App struct changes
```rust
pub struct App {
// ... existing fields ...
/// Scroll offset for the Help screen content
pub help_scroll: usize,
/// Currently selected role index in the Roles section
pub role_cursor: usize,
/// Currently selected transport line index in the Transport section
pub transport_cursor: usize,
/// Whether the seed entry popup is in "generate" mode
pub seed_generate_mode: bool,
// Remove: wizard_stage, wizard_choice, wizard_default_*,
// wizard_role_name, wizard_path, wizard_otp_dir,
// wizard_otp_name, wizard_roles_created
// Add: role_add_stage (for the add-role popup flow)
pub role_add_stage: RoleAddStage,
pub role_add_input: InputField,
pub role_add_choice: i32,
}
pub enum RoleAddStage {
PresetMenu,
NameEntry,
CurveSelect,
PathEntry,
OtpDir,
OtpName,
Confirm,
}
```
## Server changes
Currently `ServerContext` supports only one listen mode at a time. The
transport screen uses radio-button behavior: only one transport can be
active at a time. Toggling one on turns the others off. This matches
the current C behavior and avoids server architecture changes.
Additionally, `process_request` must return an activity log message
alongside the JSON response (see "Activity log format" above). Change
the return type from `String` to `(String, String)` where the first
element is the JSON response and the second is the activity description.
`handle_one` returns this message to the caller so the TUI can log it.
## Event loop changes
The `run()` loop stays the same structure (draw → poll key → service
server). The key change is that `service_server()` is called only when
the screen is `Main`. The overlay screens (`AddRole`, `Help`) pause
server processing while the user is actively configuring. The startup
popups (`SeedEntry`, `SeedDisplay`) also pause server processing since
the server hasn't started yet.
```mermaid
flowchart TD
Start[Start] --> SeedEntry[Popup: Seed Entry]
SeedEntry -->|g| SeedDisplay[Popup: Show Generated Mnemonic]
SeedDisplay -->|Enter| Main
SeedEntry -->|Enter valid phrase| Main[Main Screen]
Main -->|A| AddRole[Popup: Add Role preset menu]
AddRole -->|confirm| Main
Main -->|L| Help[Help Screen - full screen]
Help -->|ESC/B| Main
Main -->|Q| Quit[Quit]
```
Note: Transport and Roles are not separate screens — they are sections
on the main screen. The user tabs/arrow-keys between the 4 transport
lines and toggles them directly on the main screen. Roles are added
and deleted directly in the Roles section.
## Key bindings summary
### Main screen — global commands
| Key | Action |
|-----|--------|
| `L` | Open Help screen |
| `Q` / `ESC` | Quit |
| `↑` / `↓` | Scroll activity log |
### Transport section (on main screen)
| Key | Action |
|-----|--------|
| `TAB` / `↑` / `↓` | Move between transport lines |
| `ENTER` | Toggle selected transport on/off |
| `U` | Toggle Unix Socket on/off |
| `B` | Toggle Qube bridge on/off |
| `F` | Toggle FIPS on/off |
| `H` | Toggle HTTP on/off |
### Roles section (on main screen)
| Key | Action |
|-----|--------|
| `A` | Add role (opens AddRole popup with preset menu) |
| `D` | Delete the currently selected role (immediate, no confirmation) |
| `↑` / `↓` / `TAB` | Select role |
### Activity section (on main screen)
| Key | Action |
|-----|--------|
| `C` | Clear the activity log |
| `↑` / `↓` | Scroll activity log |
### Help screen
| Key | Action |
|-----|--------|
| `↑` / `↓` | Scroll help content |
| `PAGE UP` / `PAGE DOWN` | Scroll help content by page |
| `ESC` / `B` | Back to main |
## Files to change
| File | Change |
|------|--------|
| [`src/tui.rs`](../src/tui.rs:1) | Major rewrite: new `Screen` enum, startup popup for seed entry, AddRole popup, Help overlay, Transport and Roles as inline sections on main screen with toggle buttons, collapsed borders, underlined-key-letter command hints at bottom of each section, centered title. Remove `WizardStage`/wizard flow, `Connections` screen, Lock screen, Commands screen, Roles overlay screen. Remove session/derived from Information. Unix addresses without @. |
| [`src/main.rs`](../src/main.rs:129) | Minor: `App::new` call stays the same. The `listen_override` path may need adjustment since transport is now chosen from the main screen, not a setup screen. |
| [`src/server.rs`](../src/server.rs:80) | Change `process_request` return type to `(String, String)` — JSON response + activity log message. `handle_one` returns the activity message to the caller. Construct the activity message from caller_id, method, role_name, concrete_path, verdict, and source_label (matching C format). |
## Implementation steps
1. **Update `Screen` enum** — replace the seven screens with the new
set: `SeedEntry`, `SeedDisplay`, `Main`, `AddRole`, `Help`.
(No separate Transport, Roles, Lock, or Commands screen — Transport
and Roles are sections on Main, Lock is removed entirely, Commands
replaced by Help.)
2. **Update `App` struct** — remove wizard fields, add `cmd_cursor`,
`role_cursor`, `transport_cursor`, `seed_generate_mode`,
`RoleAddStage` enum and fields. Update `App::new` to start on
`Screen::SeedEntry`.
3. **Implement seed entry popup** — `draw_seed_entry()` renders a
centered popup. `handle_seed_key()` processes input: `g` → generate,
Enter → load mnemonic, derive keys, start server, transition to
`Main`. `draw_seed_display()` shows the generated phrase.
4. **Rewrite `draw_main`** — title is `Signer v0.0.1` centered on its
own line (use `Line::from(...).centered()`, no border box, no "Main
Menu"). Use `MergeStrategy::Exact` + `Spacing::Overlap(1)` for
collapsed borders. Left column has three sections: Information (top),
Transport (middle), Roles (bottom). Right column has Activity
(scrollable, newest first). Information section shows "signer name"
(renamed from "client name") + transport addresses. Remove the
Connections sub-panel.
5. **Implement Transport section on main screen** — renders 4
toggle-button lines, each showing `[x]` or `[ ]` indicator plus
the transport name with the key letter underlined (`U̲nix Socket`,
`Qube b̲ridge`, `F̲IPS`, `H̲TTP`). The `▸` cursor shows the
selected line (`transport_cursor`). Active transport is also shown
in bold/reversed. Tab/Up/Down moves between lines, Enter or the
underlined key letter (`U`/`B`/`F`/`H`) toggles that transport
(radio-button: one active at a time, toggling restarts the server).
No separate command line for this section. All key commands on the
main screen must be unique: U, B, F, H (transport), A, D (roles),
C (clear activity), L (help), Q (quit).
6. **Implement Roles section on main screen** — renders the role table
with a selection cursor (`role_cursor`). `A` opens the `AddRole`
popup (preset menu → name → path → confirm), `D` deletes the
currently selected role immediately (no confirmation), `↑`/`↓`/Tab
moves the cursor. On add/delete, re-derive keys.
7. **Implement Help screen** — `draw_help()` renders a scrollable
`Paragraph` describing what the app does, what transports are, what
roles are, and listing the key commands at the end. Track a
`help_scroll` offset. `handle_help_key()`: Up/Down (and Page
Up/Down) scroll the content, ESC/B returns to main. A scrollbar is
shown when content overflows. This is a reference screen — no
actions executed from here.
8. **Update key command bars** — each screen's bottom bar shows the
relevant key bindings for that screen. Use underlined-first-letter
word hints (e.g. `Q̲uit`, `R̲oles`) instead of "Q quit" style.
Replace the old `key_span` helper with a new `cmd_hint` helper that
produces a `Span` with the key command letter underlined (which may
not be the first letter of the word).
9. **Update activity log format** — change `ServerContext::process_request`
to return `(String, String)` (response + activity message). Construct
the activity message from `caller_id`, `method`, `role_name`,
`concrete_path`, `verdict`, and `source_label` matching the C format:
`<caller_id> <method>(<role>[,<path>]) <verdict>:<source>`. Update
`handle_one` to return the activity message. Update
`service_server()` in `App` to log this message instead of
"request handled".
10. **Update `run()` loop** — service server only on `Main` screen
(all overlay screens pause server processing). Update the
`handle_key` dispatch for the new screen enum.
11. **Update `main.rs`** — adjust `App::new` call if needed. The
`listen_override` path: if `--listen` is given, skip seed entry
popup and go straight to main with the specified transport. But
still need a mnemonic — so `--listen` with interactive mode should
still show the seed entry popup, then go to main with the transport
pre-selected.
12. **Test** — `cargo test` (unit tests unaffected). Manual test:
start signer, verify seed entry popup, verify main screen with
collapsed borders and centered title, verify Roles add/remove,
verify Transport toggle (4 lines, tab navigation), verify Help
screen shows all key commands, verify activity log shows detailed
request info, connect with `nsigner_client`.
## What stays the same
- `ActivityLog` struct and ring-buffer logic.
- `InputField` widget and `edit_key` helper.
- All non-TUI code: `dispatcher.rs`, `role_table.rs`, `mnemonic.rs`,
`key_store.rs`, etc. (server.rs gets a return-type change but its
logic stays the same).
- The event loop structure (draw → poll → service).
- Non-interactive / headless mode (`run_headless`).
## What gets removed
- `Screen::Unlock`, `Screen::GenerateMnemonic`, `Screen::RoleWizard`,
`Screen::TransportSelection`, `Screen::Connections`, `Screen::Lock`,
`Screen::Commands`, `Screen::Roles` — replaced by the startup popup,
the AddRole popup, the Help overlay, and inline sections. Lock
functionality is removed entirely. Commands screen replaced by Help
screen. Roles screen replaced by an inline Roles section.
- `WizardStage` enum and all wizard-related fields/methods.
- `draw_unlock`, `draw_generate`, `draw_wizard`, `draw_transport`
(old full-screen versions), `draw_connections_in`, `draw_lock_in`,
`handle_lock_key`, `draw_roles` (old overlay version).
- The `> Main Menu` text in the title bar.
- The bordered title bar box — title is now a plain line.
- The old `key_span` helper that produced "Q quit" style hints —
replaced by underlined-key-letter word hints.
- Session and derived count rows from the Information section.
- The `@` symbol prefix from Unix address display.
- The `R` (open Roles screen) and `E` (clear) key commands — replaced
by inline Roles management and `C` for clear.