UX Guidelines — terraform-ui
1. Layout Structure
Project: ../medprev-cloud-iac ╔╦╗╔═╗╦ ╦╦
Chdir: modules/sa-east-1 ║ ╠╣ ║ ║║
Workspace: default ╩ ╚ ╚═╝╩
┌────────────────────────────────────────────────────────────────────────────┐
│ :context │
└────────────────────────────────────────────────────────────────────────────┘
┌──────────────── State Browser (30/1549) [3/30] ────────────────────────────┐
│ module.networking.aws_vpc.main ▲ │
│ module.networking.aws_security_group.web ┃ │
│ module.compute.aws_launch_template.app ┃ │
│ module.compute.aws_iam_role.app │ │
│ module.storage.aws_s3_bucket.logs ▼ │
│ │
│ d delete e edit t taint T untaint ! batch │
└────────────────────────────────────────────────────────────────────────────┘
Enter inspect / filter Space pin ^t flat ^r refresh q back terraform
- Header (3 lines): left=Project/Chdir/Workspace, right=ASCII logo. Always visible.
- Command bar: bordered
:input, visible only when active. - Content: bordered box, view title + count embedded in top border.
- Footer: single hint line (left), binary name right-aligned faint. Shows only UI/navigation keys.
- Actions bar: inside the bordered frame, pinned to bottom. Shows terraform mutation keys as button chips.
- Scroll indicators: scrollbar gutter (right edge) +
[cursor/navigable]counter in title bar. - No separators — borders handle visual separation.
1b. Standalone Mode Layout
When invoked as tfui <command> (e.g., tfui plan, tfui state), the TUI runs in standalone mode with minimal chrome:
my-infra │ modules/sa-east-1 │ production tfui
┌─────────────────────────────────────────────────────────────────────────────┐
│ plugin content fills the screen ▲ │
│ ... ┃ │
│ ... │ │
│ ... ▼ │
│ │
│ d delete e edit t taint T untaint │
└─────────────────────────────────────────────────────────────────────────────┘
Enter inspect / filter Space pin ^t tree ^r refresh q quit terraform
Differences from full TUI
| Aspect | Full TUI | Standalone |
|---|---|---|
| Header | 3-line (project/chdir/workspace + ASCII logo) | 1-line (project/chdir/workspace left, tfui right) |
| Border | Content border with title | No border |
| Navigation | : command mode, home screen, plugin switching |
Disabled |
q key |
Go home (or quit from home) | Quit app |
esc key |
Pop frame / deactivate | Pop frame / quit when at root |
| NavPush | Works (plan→apply, state→taint) | Works identically |
| Output | None | stdout on exit (via StdoutEmitter interface) |
Standalone rendering
- TUI renders on stderr (via
tea.WithOutput(os.Stderr)) - stdout stays clean for pipe output
- On quit: plugin’s
Output(json)is written to stdout - Alt-screen on stderr: terminal restores on exit
Standalone navigation rules
:command mode: disabled (no inter-plugin navigation)C(context switch): disabledq: clears sub-frames first, then quitsDeactivateMsgwith empty navStack: quits the appNavigateMsgto non-NavPush plugins: rejected- NavPush sub-navigation works normally (plan can push apply, state can push taint)
2. Information Architecture
| Location | Content |
|---|---|
| Header left | Project (+ pinned count), Chdir, Workspace |
| Header right | ASCII logo (brand identity) |
| Content border title | View name + (filtered/total) count + [cursor/navigable] position |
| Actions bar (inside frame) | Terraform mutation keys as button chips (cyan bg, black text) |
| Scroll gutter (inside frame) | ▲ top cap, ┃ thumb, │ track, ▼ bottom cap |
| Footer (hint bar) | UI/navigation keys only (from frame’s Hints()) |
| Command bar | : input with autocomplete matches |
3. Keybinding Conventions
Global keys (work everywhere)
| Key | Action |
|—–|——–|
| q | Back to home / quit |
| : | Command mode (:q quit — guarded during ops, :q! force quit) |
| / | Filter mode |
| C | Context dashboard (plugin switch) |
| Esc | Exit sub-state (filter, detail, overlay) |
| ctrl+c | Force quit |
| ctrl+s | Screen capture to debug log (hidden) |
ctrl+char for action shortcuts
| Key | Action |
|—–|——–|
| ctrl+t | Toggle tree/flat view |
| ctrl+w | Toggle line wrap |
Navigation
| Key | Action |
|—–|——–|
| j / ↓ | Move down |
| k / ↑ | Move up |
| g | Jump to start |
| G | Jump to end |
| Enter | Inspect (leaf) / expand (branch) |
| Space | Pin/unpin |
Tree-specific
| Key | Action |
|—–|——–|
| [ | Collapse all |
| ] | Expand all |
| Enter on branch | Toggle expand/collapse |
| Enter on leaf | Inspect |
Plugin activation (home screen only)
Single plain letter: s (state), p (plan), a (apply), w (workspace), o (outputs), v (validate), ~ (console), i (init)
4. Visual Patterns
Borders
- Content:
lipgloss.RoundedBorder()with primary purple foreground - Title embedded in top border line (manual construction):
View Name (filtered/total) [cursor/navigable] - Overlays: rounded border, centered via
lipgloss.Place()
Tree connectors (2-char width)
├─ (branch, has siblings after)
└─ (last child)
│ (ancestor continuation)
Scroll Indicators
| Symbol | Meaning |
|——–|———|
| ▲ | Gutter top cap (more content above) |
| ┃ | Gutter thumb (viewport position) |
| │ | Gutter track (scrollable area) |
| ▼ | Gutter bottom cap (more content below) |
Gutter rules:
- Only appears when content overflows the viewport
- Spans content rows only (not the blank separator or actions bar)
- Lines are padded to a consistent width before appending gutter characters (prevents misalignment with styled rows)
[cursor/navigable]counter always visible in the title bar (even when content fits)- Counter reflects navigable items (respects tree collapse state and filter)
Other Indicators
| Symbol | Meaning |
|——–|———|
| * (green) | Pinned |
| ▶ | Collapsed branch |
| ▼ | Expanded branch |
| █ | Cursor in input field |
Text styles
| Style | Usage |
|——-|——-|
| Bold + primary (#bd93f9) | Titles, active keys |
| Faint (#6272a4) | Secondary info (types, counts, hints) |
| Italic + faint | Placeholder text, loading messages |
| Error (#ff5555) | Error messages |
| Success (#50fa7b) | Pin indicators, confirmations |
| Background (#44475a) | Selected row highlight, header/footer bg |
5. State Transitions & Loading
Core Principle: Honest Feedback
The user must always know what the app is doing. Every state the app enters must be visible, explicit, and traceable to a user action. No invisible work, no silent failures, no background magic.
The rule: if the app is doing something, the user sees it. If the user didn’t ask for it, the app doesn’t do it.
Loading States
When the user triggers an async operation (opens a plugin, selects a field that requires I/O, runs a command), the app transitions through:
User Action → Loading → Done (or Error)
The user sees every step. There is no shortcut.
Visual Treatment
| Operation | What the user sees |
|---|---|
| Streaming terraform command | StreamFrame — real-time log output with auto-scroll, scrollbar gutter |
| Non-streaming async (workspace list, etc.) | Faint italic text with elapsed time: "Loading workspaces... 4s" |
StreamFrame (pkg/sdk/frames/) is the standard treatment for plan, apply, and init. It:
- Auto-scrolls to the bottom as lines arrive; pauses if the user scrolls up manually
Gjumps back to the bottom and resumes auto-scroll- Stays on screen after the command finishes (success or error)
- On success: the plugin auto-navigates to its result view (plan tree, apply done state, init deactivates); the log remains accessible via
l - On error: plugin shows its error view; the log remains accessible via
l
Non-streaming operations (e.g., workspace list fetch) continue using sdk.StyleFaintItalic with elapsed time.
Cancellation in StreamFrame
^c(first press): sends SIGINT (terraform graceful shutdown); hint changes to^c force cancel^c(second press): opensConfirmFrameoverlay — “Force cancel? Infrastructure may be left in a partial state. (y/n)”y→ sends SIGINT again (brutal shutdown)n→ dismisses overlay, returns to log
Hint Bar & Actions Bar During Streaming
- Hint bar:
^c cancelwhile running;^c force cancelafter first SIGINT;Esc backwhen done - Actions bar: not rendered (no content to act on during execution)
Never show content-dependent hints during loading — there’s nothing to navigate or act on. Showing unavailable keys is lying to the user.
State Lifecycle
Every plugin that does async work follows this lifecycle:
StatusIdle → user hasn't activated yet
StatusLoading → async operation in flight (user sees loading text)
StatusDone → data available (user sees content)
StatusError → operation failed (user sees error)
Transitions are always user-initiated:
Idle → Loading: user activates the plugin or triggers a commandLoading → Done: async response arrivesLoading → Error: async response is an errorError → Loading: user pressesctrl+rto retryDone → Loading: user pressesctrl+rto refresh
The app NEVER transitions out of Idle on its own. The app NEVER refreshes without the user asking.
Error States
- Floating modal overlay with error detail (not inline text that might be missed)
- Hints:
r retry+esc back(+u force-unlockif applicable) - Error message is the raw terraform output — don’t sanitize or summarize
Other Transitions
- Context required: modal overlay prompting selection
- Destructive ops: confirmation modal with summary of what will change
- Stale data: prompt before destructive operations on data older than threshold
6. Filtering
| Aspect | Flat mode | Tree mode |
|---|---|---|
| Algorithm | fzf FuzzyMatchV2 | fzf with score threshold (len*17) |
| Sorting | By score (best first) | Preserves hierarchy order |
| Input chars | Alphanumeric + _ - . |
Same |
| Space key | Pin selected item | Pin selected item |
| Results | Flat list | Auto-expanded tree showing matches |
| Toggle | ctrl+t switches to tree |
ctrl+t switches to flat |
Filter rules
- Only accept:
a-z A-Z 0-9 _ - . - Brackets, special chars pass through to action handlers
Escexits filter modeEnterinspects (leaf) or expands (branch)- Backspace on empty ᗊ: stays in filter mode (doesn’t exit)
- Filter bar renders at the top of the list (above items, below any header)
7. Tree Navigation
- Default view: flat (fzf ranked list)
ctrl+t: toggle to tree (hierarchical with expand/collapse)- Tree builds from
SplitTerraform()which splits onmodule.Xboundaries - Pinned items/groups float to top within their sibling level
- Filter in tree mode auto-expands all branches to reveal matches
8. Modal/Overlay Pattern
- Render: centered box with
lipgloss.RoundedBorder()vialipgloss.Place() - Full screen replacement while active (content not visible behind)
- Captures ALL input — nothing passes through to plugins
Escor completion action dismisses- Status bar shows overlay-specific hints
- Used for: chdir picker, error display, confirmations
9. Picker Frame Pattern
A picker is an inline selection frame pushed onto a plugin’s own stack. It replaces navigating away to a separate plugin — the user stays in context, esc pops back, and selection auto-pops to the parent frame.
Core Principle: No Magic Behavior
Every action the app takes must be a direct, visible result of something the user did. The app never fetches data speculatively, never pre-loads behind the user’s back, never does work the user didn’t ask for.
Magic behavior = the app doing something without the user triggering it. Examples:
- Fetching workspace list on
Activate()before the user selects the workspace field - Pre-loading data “in case” the user might need it
- Background refreshes that silently update cached state
- Invisible network calls that consume resources and may fail silently
Explicit behavior = every side effect traces back to a user action. Examples:
- User selects “Workspace” → app fetches workspace list → shows loading → shows picker
- User presses
ctrl+r→ app refreshes data - User opens a plugin → app loads that plugin’s data (because they asked for it)
This means a picker with async items shows a loading state — that is honest UX. The user triggered the action, they see the consequence. A brief “Loading…” is infinitely better than hidden background magic that may silently fail, waste resources, or produce stale data the user never asked for.
Two Picker Flavors
| Flavor | Items source | On select | User sees |
|---|---|---|---|
| Sync | Already in memory (config, static list) | Push picker frame immediately | Instant list |
| Async | Requires I/O (terraform CLI call) | Kick fetch → loading text → push picker on response | "Loading..." → list |
Both share the same picker frame once items are available. The difference is only in when the fetch happens — always as a direct result of the user’s action, never speculatively.
For async pickers, the loading state follows §5 exactly:
- Form field value changes to faint italic loading text (e.g.,
"Loading workspaces...") - Hint bar reduces to
esc backonly - Picker frame pushes when data arrives
- On error: field shows error,
rretries
Behavior Contract
| Input | Action |
|---|---|
↑↓ / j/k |
Move cursor |
g / G |
Jump to start/end |
Enter |
Confirm selection → emit event → auto-pop frame |
esc |
Cancel → pop frame (no event emitted) |
q |
Same as esc |
Visual Design
▸ modules/vpc ← cursor (StyleSelected)
modules/ecs
modules/rds
▸prefix on cursor item, rendered withStyleSelected- Two-space indent on non-cursor items
- Viewport windowed via
Cursor.VisibleWindow(height) - No title inside the frame (the content border shows the parent plugin name)
Hints
[]sdk.KeyHint{
{Key: "↑↓", Description: "navigate"},
{Key: "Enter", Description: "select"},
{Key: "esc", Description: "back"},
}
Implementation
// Sync picker — items already available:
func (p *Plugin) openChdirPicker() tea.Cmd {
frame := newPickerFrame(p.members, p.current, func(selected string) tea.Cmd {
// emit event, update state
})
p.stack.Push(frame)
return nil
}
// Async picker — items require I/O:
func (p *Plugin) openWorkspacePicker() tea.Cmd {
svc := p.svc
return func() tea.Msg {
// User triggered this — fetch is a direct consequence
items, err := svc.WorkspaceList(context.Background())
return workspaceListMsg{items: items, err: err}
}
}
func (p *Plugin) Update(msg tea.Msg) (sdk.Plugin, tea.Cmd) {
switch msg := msg.(type) {
case workspaceListMsg:
if msg.err != nil { /* show error */ }
frame := newPickerFrame(msg.items, p.current, p.onSelect)
p.stack.Push(frame)
}
return p, nil
}
Anti-patterns
- Background pre-loading — fetching data the user didn’t ask for. Magic behavior: invisible side effects, wasted resources, stale data. Load when the user acts, not before.
- Navigate away to a different plugin — breaks back navigation, loses parent context
- Block in OnSelect callback — the callback returns a
tea.Cmd, never blocks the UI thread - Speculative caching — caching “in case” the user opens a picker. Only cache if the user has already seen the data and it’s still valid.
10. Home Screen
- Shows after chdir selection (or when no plugin active)
- Plugin list sorted by workflow: State, Plan, Apply, Workspaces, Outputs, Validate, Console, then decorators (Risk, Phantom, Blast Radius, Scaffold)
- Current chdir visible in header
- Direct key activation (press letter) or j/k + Enter
- Context plugin in the list (accessed via
Cor:context) — manages Project + Chdir + Workspace
11. Color Palette (Dracula)
| Name | Value | Usage |
|---|---|---|
| Primary | #bd93f9 (purple) |
Borders, keys, active elements |
| Text | #f8f8f2 (foreground) |
Main content |
| Faint | #6272a4 (comment) |
Secondary info |
| Background | #44475a (selection) |
Header/footer bg, selected rows |
| Success | #50fa7b (green) |
Pins, confirmations |
| Error | #ff5555 (red) |
Error text |
| Warning | #f1fa8c (yellow) |
Risk indicators |
| Create | #50fa7b (green) |
Plan: resources to add |
| Update | #f1fa8c (yellow) |
Plan: resources to change |
| Delete | #ff5555 (red) |
Plan: resources to destroy |
| Replace | #ff79c6 (pink) |
Plan: resources to replace |
12. Plugin View Contract
Every plugin’s View(width, height) must:
- NOT include its own title (title goes in the content border)
- NOT add padding (the bordered box handles spacing)
- Return pure content that fills the available space
- Handle empty state gracefully (show informative placeholder)
- MAY render an actions bar at the bottom (see §13) using the SDK
ActionsBarprimitive - MAY include plugin-specific contextual hints (see §13)
13. Hint Placement Rules
Three layers
| Layer | Location | Content | Source |
|---|---|---|---|
| Actions bar | Inside bordered frame (bottom, pinned) | Terraform mutation keys as button chips | Plugin’s View() using SDK ActionsBar primitive |
| Hint bar | Footer (status bar, outside border) | UI/navigation keys only | Frame.Hints() (Stackable) or Plugin.Hints() (Hintable) |
| Inline hints | Inside plugin view | Plugin-specific contextual keys not in standard vocabulary | Hardcoded in View() near relevant content |
The split rule
Bare key = actions bar. Ctrl+key or punctuation = hint bar.
The modifier (^) is the visual signal. Users don’t need to memorize categories — they just look at whether there’s a ^ prefix or not.
| Actions bar (terraform mutations) | Hint bar (UI/navigation) |
|---|---|
d delete |
Enter inspect |
e edit |
/ filter |
t taint |
Space pin |
T untaint |
^t tree/flat |
a apply |
^r refresh |
A auto-apply |
^w wrap |
m move |
^p pinned filter |
n import |
^u clear pins |
u force-unlock |
: command |
! batch |
q back |
Esc cancel |
Actions bar rules
- SDK rendering primitive — plugins own placement and visibility
- Pinned to bottom of the available frame space
- Blank line separator between scrollable content and actions bar
- Styled as two-tone button chips: bold white key on purple (
#bd93f9), label on muted purple (#644e84), single space between buttons - Left-aligned (matches content indent)
- Static per frame — content does NOT change based on cursor position
- Not rendered when plugin has no terraform actions (output, validate, version, etc.)
!batch appears only when pins > 0- In inspect/detail frame: shows single-item actions for the inspected resource
Hint bar rules
- Shows only UI/navigation keys from
HintSetvocabulary - Does NOT show
↑↓ navigate(scroll indicators teach this implicitly) - MUST be state-aware: return different hints per plugin status
- Loading state: only
q back - Error state:
^r retry+q back(+ contextual unlock hint) - Done state: full UI navigation set for that plugin
- Never show keys that don’t work in the current state
Inline hint rules
- ONLY for plugin-specific keys with no
HintSetequivalent - NEVER duplicate the hint bar or actions bar
- Format: terse
key actionseparated by double-space, styled withsdk.StyleFaintItalic - Position: near the UI element they act on (proximity = comprehension)
View content (NOT hints)
- State messages: “Loading terraform state…”, “Running terraform plan…”
- Error detail: “Error: connection refused”
- Guidance: “Run plan first to analyze risk…”
- NEVER mix keybinding text with content (“Press r to retry” is wrong)
Interface implementation
- Stackable plugins: hints come from
Frame.Hints()on the active frame - Non-stackable plugins: implement
Hintableinterface withHints() []KeyHint - Both must return state-appropriate hints (check plugin status in the method)
- Actions bar rendered by plugin
View()using the SDKActionsBarcomponent
14. Performance
- Virtual scrolling: only render visible rows (viewport window)
- Tree flatten: O(visible nodes), not O(all nodes)
- Filter: debounce unnecessary on keystroke (fzf is fast enough for 1549 items)
- State load: show elapsed time indicator after 2s
- Context discovery: cache results, don’t re-discover on every overlay open
15. Plugin UX Models
Plugins fall into two archetypes based on what the user is trying to accomplish. The archetype determines the flow, the “home state,” and how results are presented.
Data plugins (browse & act)
Intent: The user wants to explore data and optionally act on items.
Home state: A list/tree of items (always present after loading).
Examples: state, plan, output, validate, risk, phantom, blast-radius
Flow:
Activate → Loading → List/Tree (home state)
├── ↑↓ navigate, / filter, Enter inspect, Space pin
├── Action key (d, t, m, n) → confirmation → execute → refresh list
└── ctrl+r → reload data
Characteristics:
- The user lingers here — this is a workspace
- Results (the list) ARE the home state
- Actions are targeted (cursor item or pinned set via
!) - After an action completes, stay on the list (updated)
Action plugins (configure & execute)
Intent: The user wants to perform a one-shot operation.
Home state: A configuration form (the thing the user tweaks before executing).
Examples: init, apply (when entered from plan)
Flow:
Activate → Form (home state)
├── Configure fields
└── Submit → Loading → Result (transient)
├── Success → auto-return to origin (one-shot is done)
└── Error → Enter acknowledges → back to form (pre-filled for retry)
Characteristics:
- The user doesn’t linger — they configure, execute, and leave
- Results are transient feedback, not a destination
- Success means the user’s intent is satisfied — don’t force them to dismiss a “done” screen
- Error means the user needs to fix something — return them to the form with context preserved
Enteron error = “I’ve seen this, let me fix it” (acknowledge semantics, consistent with confirm)
Choosing the archetype
| Question | Data plugin | Action plugin |
|---|---|---|
| Does the user return here to browse? | Yes | No |
| Is the result the content itself? | Yes | No (result is feedback) |
| Does the user act repeatedly? | Yes (on different items) | No (one-shot, then leave) |
| After success, should user stay? | Yes (more items to explore) | No (intent satisfied) |
UX anti-patterns for action plugins
- Lingering on a “success” screen (user must dismiss manually)
- “Re-run” as a concept (if they want to re-run, they’ll re-enter the plugin)
- Showing content-navigation hints (↑↓, /, Enter inspect) on a result message
- Using
ctrl+r refreshwhen there’s nothing to refresh (init has no data to reload)
16. Keybinding Reference Map
Design layers
| Layer | Keys | Scope | Shown in | Examples |
|---|---|---|---|---|
| Terraform verbs | bare lowercase | cursor resource | Actions bar | d delete, t taint, e edit, m move, n import, u force-unlock, a apply, A auto-approve |
| Batch | ! |
pinned set | Actions bar | ! batch palette |
| Plugin switches (home) | bare lowercase | home screen only | — (home menu) | s state, p plan, w workspace, o output, v validate, ~ console, i init |
| Plugin switches (global) | bare uppercase | anywhere | — | C context, R risk, P phantom, B blast radius |
| Interface controls | ctrl+key | view modes, reload | Hint bar | ctrl+t tree, ctrl+w wrap, ctrl+r refresh, ctrl+p pinned, ctrl+u unpin all |
| Navigation & modes | non-alpha / punctuation | navigation, mode triggers | Hint bar | / filter, [ collapse, ] expand, : command, Space pin, Enter inspect |
| Leave | q / Esc |
universal | Hint bar | q home, Esc pop sub-state |
Bare lowercase (a-z)
| Key | Assignment | Category | Context |
|---|---|---|---|
a |
apply | terraform verb / plugin switch | plan view: apply; home: activate apply |
b |
— | free | |
c |
— | free | |
d |
delete | terraform verb | state, workspace |
e |
edit | terraform verb | state, plan (opens $EDITOR) |
f |
— | free | |
g |
jump to start | navigation | lists, trees |
h |
— | free | |
i |
init | terraform verb / plugin switch | home: activate init |
j |
move down | navigation (vim alias) | ↓ is primary |
k |
move up | navigation (vim alias) | ↑ is primary |
l |
— | free | |
m |
move (rename) | terraform verb | state |
n |
new / import | terraform verb | state: import (navigates to import plugin); workspace: new |
o |
output | plugin switch | home |
p |
plan | plugin switch | home |
q |
back / home | navigation | global |
r |
— | free | note: ctrl+r is refresh |
s |
state | plugin switch | home |
t |
taint | terraform verb | state/plan: navigates to taint plugin |
u |
force-unlock | terraform verb | plan, state (only when locked) |
v |
validate | plugin switch | home |
w |
workspace | plugin switch | home |
x |
— | free | |
y |
confirm (yes) | confirmation | confirmation frames only |
z |
— | free |
Bare uppercase (A-Z)
| Key | Assignment | Category | Context |
|---|---|---|---|
B |
blast radius | plugin switch (global) | decorator |
C |
context | plugin switch (global) | always available |
G |
jump to end | navigation | lists, trees |
P |
phantom | plugin switch (global) | decorator |
R |
risk | plugin switch (global) | decorator |
A |
auto-approve | terraform verb | plan: apply without confirmation |
T |
untaint | terraform verb | state/plan: navigates to untaint plugin |
Y |
confirm (yes) | confirmation | confirmation frames only |
| All others | — | free |
ctrl+key
| Key | Assignment | Category |
|---|---|---|
ctrl+c |
force quit | emergency exit |
ctrl+h |
backspace | input (emacs convention) |
ctrl+p |
pinned filter toggle | view mode |
ctrl+r |
refresh / retry | reload data |
ctrl+s |
screen capture | debug (hidden) |
ctrl+t |
tree/flat toggle | view mode |
ctrl+u |
unpin all | pin management |
ctrl+w |
line wrap toggle | view mode |
Punctuation & special
| Key | Assignment | Category |
|---|---|---|
: |
command mode | mode switch |
/ |
filter mode | mode switch |
! |
batch actions | mode switch (only when pins > 0) |
[ |
collapse all | tree action |
] |
expand all | tree action |
Space |
pin / unpin | pin toggle |
Enter |
inspect / confirm | navigation |
Esc |
exit / cancel / pop | navigation |
← → |
horizontal pan | scroll (when wrap off) |
Aliases (redundant bindings for accessibility)
| Primary | Alias | Rationale |
|---|---|---|
↓ |
j |
vim convention |
↑ |
k |
vim convention |
Enter |
y/Y |
explicit “yes” in confirmations |
Rules
Enterconfirms: it inspects/selects, and answers yes in y/n confirmations. All confirmations share one key set — confirm =y/Y/Enter, cancel =n/N/Esc— honored uniformly byConfirmFrame,InputConfirm, and the apply prompt.Spacealways means pin — never overloaded- Aliases are never shown in hints (only primary is shown)
- Free keys should not be claimed without updating this map