diff --git a/openspec/changes/path-box/.openspec.yaml b/openspec/changes/path-box/.openspec.yaml new file mode 100644 index 0000000..ac359ee --- /dev/null +++ b/openspec/changes/path-box/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-11-03 diff --git a/openspec/changes/path-box/design.md b/openspec/changes/path-box/design.md new file mode 100644 index 0000000..aace10e --- /dev/null +++ b/openspec/changes/path-box/design.md @@ -0,0 +1,53 @@ +## Context + +See proposal.md — Why. Today `State.command_line: LineEdit` is the default sink for printable keys over the panels; `map_panel_key` computes `typing = !command_line.is_empty()` and flips the meaning of Backspace, Up/Down, Left/Right and `+`/`-`/`*` on it, and `handle_enter` runs the buffer before acting on the cursor entry. The reducer is pure (application-shell "The reducer performs no filesystem I/O"): `cd` answers from the panel listing when it can and otherwise emits `Effect::ProbePath` with a request id, applied by `apply_path_probed` only when still current. Find-file already navigates a panel and lands the cursor on a named entry through `PanelState::pending_cursor_target`, settled at `ListingComplete`. Type-ahead matches names case-insensitively by prefix on `Entry::sort_key()`. The destination dialog and `cd` share `resolve_cd_target`. `draw_text_field` renders every dialog text field; `render_command_line` already left-truncates a long prompt with `…`. + +## Goals / Non-Goals + +**Goals:** +- Reach any folder or file on disk by typing a prefix and pressing Tab a few times, then Enter. +- Land the panel cursor on the named entry, including a file, not just enter folders. +- Never change what a panel key does based on hidden text state; the panels keep their NC key map. +- Pure reducer, no UI-thread I/O; stale replies dropped as everywhere else. + +**Non-Goals:** +- A popup candidate list, completion history, fuzzy matching, or completing anything but the last segment. +- Making plain printable keys do something over the panels (a follow-up could start type-ahead). +- Any shell. Ctrl+O and the Run / F2 suspended spawn move specs but do not change. + +## Decisions + +**D1 — The path box is a panels-level input mode, not an overlay.** `State.path_box: Option, probe: Option }>` lives beside `quick_search` and the quick filter in the panels layer (application-shell precedence order item 6). While it is `Some`, the TUI routes every key through `map_path_box_key` before `map_panel_key`; keys the box does not bind are ignored, so no F-key, Alt+letter or Ctrl+chord reaches the panels and the box can never be left open under a menu it opened itself. If anything above the panels in the precedence order opens for another reason (a worker-driven dialog, the startup warning, a file-operation dialog), the box is closed and reset, as a type-ahead ends. *Alternative considered:* an `Overlay::PathBox` — rejected because overlays hide the terminal cursor, exclude the menu bar's hit map, and the box must coexist with the panels' rendering and mini-status. + +**D2 — Focus by chord or click; Tab stays the panel switch.** `Keys.focus_path` defaults to Ctrl+G (Ctrl+L is info mode; Ctrl+Y is freed by this change but reads as "clear", not "go"). A click on either box dispatches `PathBoxFocus(side)`, which also makes that panel active so the box, cursor and mini-status agree. On focus the text is `cwd` plus the platform separator with the caret at the end (`LineEdit::set_text_caret_end`). Inside the box: `map_text_edit_key` (the full dialog set), Ctrl+V and Shift+Ins → `PathBoxPaste`, Tab → `PathBoxComplete { reverse: false }`, Shift+Tab (`BackTab`) → `{ reverse: true }`, Enter → `PathBoxAccept`, Esc → `PathBoxCancel`. A mouse click outside the focused box dispatches `PathBoxCancel` and is then handled normally. + +**D3 — Completion state is derived from the text and cached per folder.** On Tab the reducer splits the text at the last separator into `(folder_text, prefix)`, resolves `folder_text` with `resolve_cd_target` against the box's panel `cwd` (so `..\`, `D:x\`, `\\srv\share\` and relative text work) and compares it with `completion.dir`. If equal and `names` is loaded it advances the cycle; otherwise it starts a new completion: if the folder equals either panel's `cwd` and that listing is `Complete`, the names come from that listing at once; else it emits `Effect::ListCompletions { request, dir }` and the mini-status shows `Reading…` until `Command::CompletionsListed { request, dir, names: Option> }` arrives. `names` holds every readable child, no `..`; the worker reads through `FsReader::read_dir` in a new `listing::list_children_for_completion`. Candidates are the names whose `sort_key()`-style lowercase form starts with the lowercase prefix, ordered by `(is_file, lowercase name)` — directories first, then files. This is a small comparator of its own: the panel's `cmp_entries` / `cmp_by_name` deliberately interleave directories and files (classic NC order) and are not reused here. The prefix used for matching is frozen when the cycle starts so repeated Tabs cycle instead of narrowing. Applying a candidate replaces the last segment with the on-disk name and appends a separator for a directory. Any text mutation other than Tab/Shift+Tab sets `index = None` but keeps `names` while `dir` is unchanged, so backspacing and retyping do not re-read. A reply whose `request` is not outstanding, or whose `dir` differs from the folder the current text names, is discarded; Tab during an outstanding read is ignored; `names: None` (unreadable) yields `no matches`. + +**D4 — Enter reuses the cd probe and the find-file cursor landing.** `PathBoxAccept` resolves the whole text (ignoring one trailing separator) with `resolve_cd_target`. If it equals the panel `cwd` → close the box. If `parent_path` is `None` (drive root, UNC share root, `/`) → `begin_listing(side, path)`, close. Otherwise `(parent, name)`: when `parent` equals the box's panel `cwd` and its complete listing holds an entry matching `name` case-insensitively → immediate; when it equals the other panel's complete listing → likewise; else `Effect::ProbePath` with `ProbeIntent::Select { parent, name }` and `pending_note = Checking …`. The reply is applied only if `path_box` is still focused on that side with the same text and the probe id is current; otherwise it is dropped silently. On success: if `parent != cwd`, `begin_listing(side, parent)` then `pending_cursor_target = Some(name)`; when already there, move the cursor directly with the same lookup `jump_to_prefix` uses, clearing a quick filter that hides the target; close the box. On failure: `last_error` names the path (filesystem-error-handling "Error messages name the affected path"); the box keeps focus and text. `ProbeIntent::{Cd, Delete}` are deleted; `PendingProbe { request, side, cwd, path, intent }` keeps its shape. + +**D5 — `pending_cursor_target` matches case-insensitively as a fallback.** `ListingComplete` first looks for an exact `OsString` match (find-file's existing contract), then for the first entry whose `sort_key()` equals the lowercased target. A probe-confirmed `readme.MD` then lands on `README.md` without the worker canonicalising the name. Find-file is unaffected because its names come from disk. + +**D6 — Rendering splits the row at the panel divider and reuses the field renderer.** `layout::compute` keeps `cmdline` as the row above the key bar (core's `panel_viewport_rows` is untouched); the view derives two rects from the left and right panel rects on that row. An unfocused box draws `` in `Role::CommandLine`, left-truncated with `…` when wider than the box. `left_truncate_with_ellipsis` exists as a private function in both `views/command_line.rs` and `views/header.rs`; it is promoted to one shared helper in `views/mod.rs` and applied to the whole box width rather than half the row. The focused box draws its `LineEdit` through `draw_text_field` in the dialog text-field style and returns the caret, which becomes `RenderOutput.cursor`; with no focused box and no dialog field the terminal cursor is hidden. The mini-status override of the box's panel shows `k/n matches`, `no matches` or `Reading…` while a completion is active, in the slot the type-ahead pattern uses today. The hit map records `path_boxes: [Rect; 2]`. + +**D7 — Keys over the panels lose their "typing" dependence.** `map_panel_key` drops `typing`: Backspace is always `ParentDir`, Up/Down always move the cursor, Left/Right are unbound, `+`/`-`/`*` always group-select, Ctrl+Backspace/Ctrl+H are unbound, Ctrl+V and bracketed paste over unfocused panels are ignored, and a plain printable key over the panels is ignored (no sink). Esc keeps requesting quit over the panels; inside a focused box it cancels the box. Alt+letter type-ahead and the Ctrl+P quick filter are unchanged; focusing the box ends type-ahead. + +**D8 — History: the file keeps its shape, the app stops using commands.** `HistoryFile { commands, directories }` and the merge-on-write writer stay, so two instances still preserve each other's data and the Ctrl+J directory history is untouched. `State.history` / `history_cursor`, `config::push_history`, the Alt+F8 dialog, the Commands → History item and `MenuAction::History` are removed; `Effect::PersistHistory(HistoryFile)` keeps its shape, but the `HistoryFile` it carries always has an empty `commands`; the writer's existing merge (`merge_history_files`, fuzzy-jump "Two instances keep each other's history") keeps the file's `commands` array through unchanged. + +**D9 — Config keys.** `key.focus_path` joins the overridable bindings (default `ctrl+g`). `key.paste_name`, `key.paste_path` and `key.clear_line` are no longer recognised, so an old `config.toml` that sets them gets the existing unrecognised-key startup warning (configuration-files "Malformed or unrecognized configuration lines produce startup warnings") — honest, and it tells the user what to delete. No migration. + +**D10 — Interaction with `auto-refresh` (in flight).** Completion also treats a streaming listing as unavailable (path-box "Completion candidates come from a panel listing when possible"): only a `Complete` listing answers, so a partial listing never yields a partial candidate set. Its design D3 eligibility list (no overlay, drag or type-ahead) gains "no focused path box", so an automatic re-read never replaces the listing a completion cycle was answered from. Whichever change is built second adds the guard. + +**D11 — Scenario titles that still say "command line" are kept on purpose.** `openspec validate` rejects a MODIFIED block that drops or renames a scenario, so the restated requirements keep titles such as "Backspace on empty command line goes to the parent" and "Non-empty command buffer takes precedence" while their bodies describe the path box. A later housekeeping change may retitle them directly in the main specs. + +## Risks / Trade-offs + +- [Breaking for `cd` / `del` / `rmdir` users] → `del` / `rmdir` have F8 and Delete; `cd` is exactly what the box does better. README and Help say so. +- [Printable keys over the panels do nothing] → Matches the dedicated-focus choice; a later change can route them into type-ahead. Help's "Path box" topic and the README state Ctrl+G up front. +- [Completion reads a whole directory] → One read per folder per cycle on a worker thread, cached while the folder text is unchanged; a huge folder costs one enumeration, as listing it does. An unreachable share shows `Reading…` and never blocks input; a reply for abandoned text is dropped. +- [Snapshot churn] → The prompt row appears in 66 of the 138 `.snap` files; they regenerate with the change. +- [Ctrl+G availability] → A plain control code delivered by every terminal; rebindable anyway. +- [Case folding] → Prefix and target matching use `sort_key()` (Unicode lowercase); on case-sensitive filesystems two entries differing only by case both appear as candidates, and Enter lands on the exact match first (D5). + +## Open Questions + +None blocking. Follow-ups considered: start type-ahead from a plain printable key; a candidate popup; a small `Reading…` indicator in the box itself. diff --git a/openspec/changes/path-box/proposal.md b/openspec/changes/path-box/proposal.md new file mode 100644 index 0000000..d3e096c --- /dev/null +++ b/openspec/changes/path-box/proposal.md @@ -0,0 +1,50 @@ +# Change: path-box + +## Why + +The row above the F-key bar is a Norton Commander command line, but FileCommand never runs typed text through a shell: the buffer only recognises `cd`, `del` and `rmdir`, and everything else is an error. What remains is a typing sink that silently changes what Backspace, Up/Down, Left/Right, Enter and `+`/`-`/`*` do depending on whether it holds text, plus history, paste-name and clear-line machinery that exists only to serve those three verbs. Meanwhile the one thing the row is good for — getting to a path by typing it — has no completion, so a long path must be typed in full, and the panel can only *enter* a folder, never land on a file. + +This change replaces the command line with two **path boxes**, one under each panel, that show the panel's folder and complete folder and file names on Tab. Pressing Enter on a valid path puts the panel cursor on that entry. Everything else the command line did is removed. + +## What Changes + +- **The command-line row becomes two path boxes.** The row above the F-key bar splits at the panel divider. Each box shows its panel's current folder followed by a path separator (`C:\Github\FileCommand\`) and follows the panel through navigation, tab switches and panel swaps. Unfocused boxes are drawn in the existing `commandline` theme role; a long folder left-truncates with a leading `…`. +- **A box must be focused to type into it.** Ctrl+G (rebindable via `key.focus_path`) focuses the active panel's box; a mouse click on either box focuses that box and makes its panel active. While a box has focus it owns the keyboard and the terminal cursor and is drawn in the dialog text-field style. Esc, or a click outside the box, returns focus to the panel and resets the box to the panel's folder. While no box is focused the panels keep every key meaning they have today — Tab still switches panels, Backspace still goes to the parent, `+`/`-`/`*` always select — and plain printable keys over the panels are ignored. +- **Tab completes a path segment.** The text before the last separator names a folder; the text after it is a case-insensitive prefix. Tab replaces that last segment with the next child of the folder whose name starts with the prefix (folders first, then files, each sorted by name); Shift+Tab goes back; both wrap. A completed folder gets a trailing separator so the next Tab descends into it. The panel's mini-status shows `3/12 matches`, `no matches`, or `Reading…` while the folder is being read. Candidates come from a panel's own listing when the folder is that panel's current folder, otherwise from a directory read on a worker thread; a reply that no longer matches the box text is discarded. +- **Enter selects the entry.** The text resolves to an absolute path with the same rules the Copy/Move destination field uses (relative, `..`, drive-relative `D:x`, UNC). Its existence is answered from a panel listing when possible, otherwise by the existing off-thread path probe with a `Checking …` note. On success the panel navigates to the entry's parent folder if it is not already there and lands the cursor on the entry; a filesystem root, which has no parent, is entered instead; text naming the panel's own folder just closes the box. Focus returns to the panel. On failure the panel shows a not-found error and the box keeps focus and text. +- **BREAKING — removed:** the `cd`, `del` and `rmdir` builtins; command history (Up/Down recall, the Alt+F8 history dialog, Commands → History); Ctrl+Enter / Ctrl+] paste-name/paste-path; Ctrl+Y clear-line; the `key.paste_name`, `key.paste_path` and `key.clear_line` config keys (a config that still sets them gets the standard unrecognised-key startup warning). `history.json` keeps its format: the `commands` array is preserved on write but never appended to; directory history for Ctrl+J is unchanged. +- **Kept, re-homed:** Ctrl+O panels-on/off and the suspended-TUI shell run used by the file-action menu's Run entry and the F2 user menu (including the configurable shell) move from the `command-line` capability to `application-shell`, unchanged in behaviour. +- **Docs:** the F1 Help topic `Command line` becomes `Path box`; README key tables and the Command line section are rewritten. +- **Non-goals:** a candidate popup list; completion history; routing plain printable keys into the box or into type-ahead; any change to Ctrl+J fuzzy jump, Alt+letter type-ahead or the Ctrl+P quick filter. + +## Capabilities + +### New Capabilities + +- `path-box`: the two path boxes under the panels — their rendering, focus and editing, Tab completion, Enter selection, Esc, and the `key.focus_path` binding. + +### Modified Capabilities + +- `command-line`: **retired.** Every requirement is REMOVED; Ctrl+O and the suspended shell run are re-added under `application-shell`. The capability directory is deleted when the change is archived. +- `application-shell`: ADDED "Panels on/off reveals terminal scrollback" and "Configurable shell with documented latency tradeoff" (moved verbatim from `command-line`), plus "Shell commands run only from the Run entry and the user menu" and "A suspended shell run restores the terminal" (the former command-line "Run command via shell in suspended-TUI mode", split in two and reworded for its new home); MODIFIED "Terminal ownership and restoration on every exit", "Resize handling with 60x16 hard floor and placeholder", "Quit request keys and confirmation" (Esc in a focused path box cancels the box), "Bursts of key input render once", "The reducer performs no filesystem I/O", "Slow storage never blocks the event loop", "Modal overlays are mutually exclusive by construction" (the history dialog is gone). +- `panel-navigation`: MODIFIED "Parent-directory navigation" (Backspace is unconditional), "Swap panels (Ctrl+U)", "Superseded directory listings are discarded and cancelled" (scenario wording). +- `file-action-menu`: MODIFIED "Enter on a file opens the action menu" (no command-buffer precedence), "Menu actions route to existing flows" (wording). +- `text-field-editing`: MODIFIED "Caret model and editing keys", "Paste into a text field", "Terminal paste arrives as one paste" (the path box is the panels-level text field; no exceptions list). +- `type-ahead-jump`: MODIFIED "Entering type-ahead mode with Alt+letter", "Extending the pattern with printable keys"; RENAMED "Exiting type-ahead and restoring command-line routing" → "Exiting type-ahead", with its text MODIFIED (printables are ignored after exit). +- `responsive-layout`: MODIFIED "Degraded band and per-panel breakpoints" (wording), "Chrome degradation" (path-box scrolling and truncation). +- `quick-filter`: MODIFIED "Cursor and mini-status behavior under an active filter" (drops the paste bullet). +- `help-and-about`: MODIFIED "Help topic list" (`Path box`). +- `pulldown-menus`: MODIFIED "Menu contents" (Commands loses History). +- `startup-splash`: MODIFIED "Minimum hold and key dismissal" (wording). +- `user-menu`: MODIFIED "Navigate and dismiss the user menu" (wording). +- `theme-selection`: MODIFIED "Live theme preview while the picker is open" (wording). +- `filesystem-error-handling`: MODIFIED "Error messages name the affected path" (wording). + +## Impact + +- **Core (`crates/filecommand-core`):** remove `State.command_line` / `history` / `history_cursor`, the `CommandLine*`, `PasteCursor*`, `HistoryDialog*` commands and the panels-level `TextEdit` / `RequestPaste` arms, `ProbeIntent::{Cd, Delete}`, `run_command_line` and the builtin parsers in `update/panels.rs`, `HistoryDialogState`, the History menu item. Keep `resolve_cd_target` / `parse_drive_relative` (destination dialog), `panels_own_input` (viewer), `Role::CommandLine`, the `HistoryFile` reader/writer. Add `State.path_box: Option` (new `path_box.rs`), `Command::{PathBoxFocus, PathBoxEdit, PathBoxComplete, PathBoxAccept, PathBoxCancel, PathBoxPaste, CompletionsListed}`, `Effect::ListCompletions`, `ProbeIntent::Select`, `Keys.focus_path`; `pending_cursor_target` matching gains a case-insensitive fallback. +- **TUI (`crates/filecommand-tui`):** `views/command_line.rs` → `views/path_box.rs` (two boxes, reusing `draw_text_field` and `left_truncate_with_ellipsis`); `input/mod.rs` drops the `typing` gate and all command-line arms, adds a `map_path_box_key` routed ahead of the panel map; mouse: box click; `worker.rs` gains a completion-list reader; `app.rs` stops loading command history into state (the writer keeps the file's `commands`); hitmap `cmdline` → `path_boxes`; `views/history_dialog.rs` removed. +- **Tests:** every command-line test in `update/tests/{panels,file_op,listing,overlays,routing}.rs`, `input/tests.rs`, `config.rs`, `menu.rs`, the `app.rs` burst tests, and the `snapshot_views.rs` command-line and history-dialog snapshots; 66 of the 138 snapshot files regenerate because the prompt row changes; new reducer tests for the path box; new snapshots for unfocused and focused boxes and the matches mini-status. +- **Docs:** README (the "Command line" section and its "History" subsection, key tables, `key.*` list), F1 Help topic text. +- **Persisted formats:** `config.toml` loses three `key.*` keys and gains `key.focus_path`; `history.json` format unchanged. +- **Breaking** for users who typed `cd` / `del` / `rmdir`, used Alt+F8, or rebound the removed keys. diff --git a/openspec/changes/path-box/specs/application-shell/spec.md b/openspec/changes/path-box/specs/application-shell/spec.md new file mode 100644 index 0000000..175ba98 --- /dev/null +++ b/openspec/changes/path-box/specs/application-shell/spec.md @@ -0,0 +1,437 @@ +## ADDED Requirements + +### Requirement: Panels on/off reveals terminal scrollback + +When the user presses Ctrl+O, the system SHALL leave the alternate screen to expose the host terminal's scrollback containing prior command output, and any subsequent key press SHALL return to the alternate screen and redraw. The system SHALL NOT maintain its own command-output buffer; the visible output history is whatever the host terminal retains. + +#### Scenario: Ctrl+O leaves the alternate screen +- **WHEN** the user presses Ctrl+O +- **THEN** the app leaves the alternate screen so the terminal's scrollback (including prior command output) is visible + +#### Scenario: Any key returns to panels +- **WHEN** the terminal scrollback is showing after Ctrl+O and the user presses any key +- **THEN** the app re-enters the alternate screen and redraws the panels + +### Requirement: Shell commands run only from the Run entry and the user menu + +The system SHALL NOT run typed text through a shell. The suspended-TUI shell-spawn mechanism SHALL be reachable only from the file-action menu's Run entry and the F2 user menu, each via its own trigger. Enter on an executable target (PATHEXT match or `.lnk`) SHALL NOT spawn the target directly; it SHALL open the file-action menu for that entry, whose Run entry uses this spawn path. + +#### Scenario: Enter on an executable opens the menu instead of spawning +- **WHEN** the user presses Enter with the cursor on `setup.exe` +- **THEN** no child process spawns and the file-action menu opens for `setup.exe` +- **AND** activating the menu's Run entry spawns `setup.exe` via the suspended-TUI path + +#### Scenario: Typed text never reaches a shell +- **WHEN** a path box has focus, reads `C:\NORTON\dir` and the user presses Enter +- **THEN** no shell process is spawned; the text is treated as a path per `path-box` + +### Requirement: A suspended shell run restores the terminal + +Running a command through the suspended-TUI mechanism SHALL leave raw mode and the alternate screen, spawn the child inheriting stdio in the panel directory, wait for it to exit, prompt the user to press a key, then re-enter the alternate screen and raw mode and redraw. Terminal restore SHALL be idempotent so a failing or panicking child cannot leave the terminal in raw mode or the alternate screen. + +#### Scenario: Terminal restored after a failing child +- **WHEN** a child spawned via the Run entry or F2 user menu exits with an error or the spawn fails +- **THEN** the TUI is restored to the alternate screen and raw mode exactly once, and the app does not crash + +### Requirement: Configurable shell with documented latency tradeoff + +The system SHALL construct the shell invocation from a configurable shell setting, defaulting on Windows to `cmd.exe /C` for minimal spawn latency. The `config.toml` `shell =` key SHALL select an alternate shell such as PowerShell or `pwsh`, and the configuration SHALL document that PowerShell adds roughly 200 ms or more of spawn latency per command. The shell invocation SHALL be constructed as `shell + args + user text` in `filecommand-core` so it is unit-testable without a terminal. + +#### Scenario: Default shell is cmd.exe on Windows +- **WHEN** no `shell =` value is configured on Windows and a user-menu entry runs `dir` +- **THEN** the invocation is built from `cmd.exe /C` with the entry's text appended + +#### Scenario: Configured PowerShell shell is used +- **WHEN** `config.toml` sets `shell = "powershell"` and the user runs a user-menu entry +- **THEN** the invocation is built from the configured PowerShell executable rather than `cmd.exe` + +#### Scenario: Command construction is terminal-independent +- **WHEN** a unit test constructs a command line for the text `dir` with the default shell +- **THEN** the constructed invocation and working directory are produced by `filecommand-core` without requiring a terminal or an actual spawn + +## MODIFIED Requirements + +### Requirement: Terminal ownership and restoration on every exit + +The system SHALL acquire the alternate screen, raw mode, and (when enabled) mouse capture on startup and guarantee their release on every exit path — normal quit, error, and panic — via an RAII guard, so the user's terminal is never left in raw mode, on the alternate screen, or with mouse capture active after the process ends. When the TUI is suspended to run a shell command, an external editor, or the scrollback view, mouse capture SHALL be released before the alternate screen is left and re-acquired on resume. + +#### Scenario: Terminal acquired on startup + +- **WHEN** the application starts +- **THEN** it enters the alternate screen, enables raw mode, and enables mouse capture when configured + +#### Scenario: Terminal restored on normal exit + +- **WHEN** the application exits normally (for example via F10 quit) +- **THEN** mouse capture is disabled, raw mode is disabled, and the alternate screen is left before the process terminates + +#### Scenario: Terminal restored on error exit + +- **WHEN** the application exits because of an error after the terminal was acquired +- **THEN** the RAII guard still disables mouse capture and raw mode and leaves the alternate screen + +#### Scenario: Suspended shell run gets a normal terminal + +- **WHEN** the user runs a command from the file-action menu's Run entry or the F2 user menu +- **THEN** mouse capture is released before the child process runs and re-enabled after the TUI resumes + +### Requirement: Resize handling with 60x16 hard floor and placeholder + +The system SHALL reflow the UI on terminal resize events, laying out the interface at any size at or above the 60x16 hard floor, and MUST draw a `screen.placeholder` "terminal too small" message instead whenever the terminal is below that floor, using a single size check that governs both normal and splash states. The placeholder message SHALL name the floor: "resize to at least 60x16". Between the floor and the 80x24 nominal size the interface renders in the degraded forms defined by the `responsive-layout` capability; at or above 80x24 it renders at full fidelity. + +The placeholder SHALL be drawn over the active screen without discarding it. While the terminal is below the floor, the following SHALL persist unchanged and SHALL reappear when the terminal is enlarged to at or above the floor: + +- the panels and their tabs, cursor, selection, quick filter, and a focused path box with its text; +- the built-in editor: its buffer including unsaved edits, caret position, insert/overwrite mode, and modified state; +- the F3 viewer: its file, position, and text/hex and wrap modes; +- a file-operation setup dialog with its typed input; +- a running file-operation job, with its progress, overwrite-conflict, or error-recovery dialog; +- the end-of-job skipped-files summary; +- any open pull-down menu, dialog, or overlay. + +The startup splash is the only exception: it SHALL be dismissed when the terminal shrinks below the floor and SHALL NOT return when the terminal is enlarged. + +Worker results SHALL continue to be applied while the terminal is below the floor. These include directory-listing events, file-operation progress, conflict, error, and completion events, and replies to requests made before the shrink. A job that completes while the placeholder is shown therefore still re-reads the affected panels, and a conflict or error raised while it is shown waits for the user's answer. + +While the terminal is below the floor, input SHALL NOT reach the screen hidden under the placeholder: + +- Mouse input SHALL be ignored, except that while the quit-confirmation dialog is open over the placeholder its buttons SHALL stay clickable. +- F10 and Esc SHALL open the quit-confirmation dialog, which is drawn over the placeholder. The exception is when the built-in editor is the hidden screen: there F10 and Esc SHALL be ignored, so unsaved edits are never discarded without the editor's save-on-exit prompt. +- While the quit-confirmation dialog or the startup-warning dialog is open over the placeholder, that dialog's own keys SHALL work as usual. +- Every other key SHALL be ignored. + +While the terminal is below the floor, the screen under the placeholder does not own input, so these rules take precedence over that screen's own key handling, including Esc's cancel/dismiss meaning in a hidden menu or dialog under "Quit request keys and confirmation". + +#### Scenario: Reflow at or above the floor + +- **WHEN** the terminal is resized to a size at or above 60x16 +- **THEN** the UI reflows and lays out its regions to the new dimensions + +#### Scenario: Degraded band renders panels, not the placeholder + +- **WHEN** the terminal is 70x20 — below the 80x24 nominal size but at or above the floor +- **THEN** the panels, path boxes, and key bar render in their degraded forms and the placeholder is not shown + +#### Scenario: Placeholder below the floor + +- **WHEN** the terminal is below 60 columns or below 16 rows +- **THEN** the "terminal too small" placeholder message is drawn instead of the normal layout, naming the 60x16 floor + +#### Scenario: Shrinking below the floor during splash + +- **WHEN** the terminal shrinks below 60x16 while the startup splash is showing +- **THEN** the placeholder replaces the splash, and the splash does not return when the terminal is enlarged again + +#### Scenario: Recovery when resized back up + +- **WHEN** the terminal is enlarged from below the floor back to at or above 60x16 +- **THEN** the normal layout is drawn again in place of the placeholder + +#### Scenario: Unsaved editor buffer survives a shrink + +- **WHEN** the built-in editor has unsaved edits with the caret on line 12 in overwrite mode, and the terminal shrinks to 50x12 and is then enlarged to 80x24 +- **THEN** the editor is shown again with the same buffer contents, the caret on line 12, overwrite mode, and the modified indicator +- **AND** nothing was written to disk and no prompt was raised while the placeholder was shown + +#### Scenario: Viewer position and mode survive a shrink + +- **WHEN** the F3 viewer shows a file in hex mode scrolled to offset 0x4000, and the terminal shrinks below the floor and is then enlarged +- **THEN** the viewer is shown again on the same file, in hex mode, at offset 0x4000 + +#### Scenario: Viewer closes normally after a shrink + +- **WHEN** the terminal shrinks below the floor and is enlarged while the viewer is open, and the user then closes the viewer +- **THEN** the panels return and the viewer no longer holds the file open + +#### Scenario: Running job keeps reporting progress while undersized + +- **WHEN** a copy job is running with its progress dialog shown and the terminal shrinks below the floor +- **THEN** the placeholder is shown, the job keeps running, and its progress events keep updating the hidden progress dialog +- **AND** when the terminal is enlarged before the job ends, the progress dialog reappears showing the job's latest progress + +#### Scenario: Conflict raised while undersized waits for an answer + +- **WHEN** a running copy job reaches an overwrite conflict while the terminal is below the floor +- **THEN** the job waits for an answer and the conflict is not resolved by default +- **AND** when the terminal is enlarged the overwrite-conflict dialog is shown for that file, and answering it resumes the job + +#### Scenario: Job finishing while undersized still refreshes the panels + +- **WHEN** a copy job completes while the terminal is below the floor +- **THEN** the panels showing the source and destination directories re-read while the placeholder is still shown +- **AND** when the terminal is enlarged the panels show the copied files, or the end-of-job summary when items were skipped + +#### Scenario: Open dialog and typed text survive a shrink + +- **WHEN** the F5 destination dialog is open with `D:\backup` typed and the terminal shrinks below the floor and is then enlarged +- **THEN** the destination dialog is shown again containing `D:\backup` with the caret where it was + +#### Scenario: Typing over the hidden editor does nothing + +- **WHEN** the built-in editor is the hidden screen while the terminal is below the floor and the user types letters, presses Enter, F2, F10, or Esc +- **THEN** the buffer, caret, and file on disk are unchanged, no save runs, and no quit-confirmation or save-on-exit dialog opens + +#### Scenario: F10 over the placeholder asks to quit + +- **WHEN** the panels are the hidden screen while the terminal is below the floor and the user presses F10 +- **THEN** the quit-confirmation dialog opens over the placeholder +- **AND** cancelling it leaves the placeholder shown and the hidden panels unchanged + +#### Scenario: Mouse input is ignored while undersized + +- **WHEN** the terminal is below the floor, no quit-confirmation dialog is open, and the user clicks or turns the mouse wheel anywhere in it +- **THEN** no command reaches the hidden screen and nothing about it changes + +#### Scenario: Quit dialog buttons stay clickable over the placeholder + +- **WHEN** the quit-confirmation dialog is open over the placeholder and the user clicks its No button +- **THEN** the dialog closes, the placeholder stays shown, and the hidden screen is unchanged + +### Requirement: Quit request keys and confirmation + +The system SHALL open a quit-confirmation dialog — never exit directly — when the user requests quit via F10, Files → Quit, or Esc over the panels. Esc SHALL request quit whenever the panels own input (no pull-down menu and no modal dialog or overlay open), regardless of an active quick filter or type-ahead mode — while a path box has focus, Esc instead cancels the box (path-box "Esc cancels the path box"); inside menus, dialogs, the viewer, and editor sub-prompts Esc SHALL retain its existing cancel/dismiss meaning. Ctrl+C SHALL NOT request quit in any context (over the panels it is the clipboard Files action — see `clipboard-export`; in the built-in editor it remains Copy). Within the quit-confirmation dialog, Esc SHALL cancel alongside the existing confirm/cancel keys; Ctrl+C SHALL have no effect there. Cancelling SHALL restore the prior context exactly — anything open stays open, and quick-filter and type-ahead state survive. Confirming while a file operation is running SHALL abort the job through its existing cancel semantics before the application exits. + +#### Scenario: Esc over idle panels asks to quit + +- **WHEN** the panels are shown with no menu, dialog, or overlay open and the user presses Esc +- **THEN** the quit-confirmation dialog opens +- **AND** confirming exits the application and cancelling returns to the panels unchanged + +#### Scenario: Esc mid-composition asks to quit and cancel preserves the text + +- **WHEN** a path box has focus with edited text and the user presses Esc +- **THEN** the box closes and resets to the panel's directory and no quit-confirmation dialog opens +- **AND** a second Esc, now over the panels, opens the quit-confirmation dialog + +#### Scenario: Esc under an active quick filter asks to quit + +- **WHEN** a quick filter is active and the user presses Esc, then cancels the dialog +- **THEN** the quit-confirmation dialog opened instead of the filter being cleared +- **AND** after cancelling, the filter pattern and narrowed listing are unchanged + +#### Scenario: Esc still cancels menus and dialogs + +- **WHEN** a pull-down menu or a modal dialog is open and the user presses Esc +- **THEN** the menu or dialog closes as before and no quit-confirmation dialog opens + +#### Scenario: Ctrl+C never asks to quit + +- **WHEN** the user presses Ctrl+C over the panels, in the viewer, with a pull-down open, or with a modal dialog open +- **THEN** no quit-confirmation dialog opens (over the panels the clipboard Files action runs; elsewhere Ctrl+C neither quits nor triggers any unmodified-key action) + +#### Scenario: Ctrl+C in the built-in editor still copies + +- **WHEN** the built-in editor is open and the user presses Ctrl+C +- **THEN** the editor's Copy action runs and no quit-confirmation dialog opens + +#### Scenario: Esc backs out of the dialog; Ctrl+C does nothing there + +- **WHEN** the quit-confirmation dialog is open and the user presses Esc +- **THEN** the dialog closes and nothing else changes +- **WHEN** the quit-confirmation dialog is open and the user presses Ctrl+C +- **THEN** the dialog stays open and the application does not exit + +#### Scenario: Quit confirmed during a running file operation aborts the job first + +- **WHEN** a copy/move/delete job is running and the user requests quit via Esc, then confirms +- **THEN** the job is aborted through the same path as its cancel action before the application exits + +### Requirement: Bursts of key input render once + +When several input events are already queued, the system SHALL apply them in order — mapping each key against the state produced by the previous one — and SHALL draw once after the burst instead of once per key. A burst SHALL end after 64 events, after about 8 ms of processing, when no further input is queued, or when a mouse event is reached; the system SHALL then draw before handling further input, so the screen never falls far behind. Applying a burst SHALL produce the same final state as applying the same keys one at a time. + +#### Scenario: Holding PgDn renders one frame per burst + +- **WHEN** five PgDn key events are queued at once in a 100,000-entry directory +- **THEN** all five page movements are applied and the screen is drawn once, showing the final position + +#### Scenario: Routing follows the state each key produces + +- **WHEN** the queued keys are F7 followed by the characters `new` and Enter while the panels are focused +- **THEN** F7 opens the Make-directory dialog, `new` is typed into its field, and Enter confirms it, exactly as if the keys had been pressed slowly + +#### Scenario: A paste delivered as key events renders once per burst + +- **WHEN** the terminal delivers a 60-character pasted path as 60 key events while a path box has focus +- **THEN** all 60 characters are inserted in order and the screen is drawn at most once for every 64 of them + +### Requirement: The reducer performs no filesystem I/O + +`core::update` SHALL NOT perform filesystem I/O of any kind, directly or through any helper it calls. That includes metadata or existence queries, directory reads, and file opens. When handling a command depends on the current state of the filesystem, `core::update` SHALL: + +- answer from the state it already holds where that state suffices, such as an entry already listed in a panel; +- otherwise return an effect requesting the check, tagged with a fresh request id; +- complete the command only when the reply command carrying that id is applied. + +A reply whose id is no longer the outstanding request SHALL be discarded without changing state. This makes the "Pure core update function" rule checkable by a unit test that runs with no filesystem at all. + +#### Scenario: A command-line target outside the listing produces a probe request, not a disk read + +- **WHEN** a unit test applies Enter in a path box reading `C:\NORTON\sub\x.txt` to `core::update` with `C:\NORTON\sub` the current directory of neither panel +- **THEN** the returned effects contain a path-probe request for `C:\NORTON\sub\x.txt` tagged with a request id, and the panel's `cwd` is unchanged +- **AND** `core::update` made no filesystem call, so the result is the same whether or not `C:\NORTON\sub\x.txt` exists on the machine running the test + +#### Scenario: The probe reply completes the command + +- **WHEN** the path-probe reply for that request id arrives reporting that the entry exists +- **THEN** applying it through `core::update` navigates the panel to `C:\NORTON\sub` and lands the cursor on `x.txt` exactly as a synchronous check would have + +#### Scenario: A superseded probe reply is discarded + +- **WHEN** a second Enter is applied before the first probe's reply arrives, and the first reply then arrives +- **THEN** the first reply changes nothing: no navigation, no error message, and no dialog + +#### Scenario: A completion read is a request, not a disk read + +- **WHEN** a unit test applies Tab in a path box reading `D:\Proj\re` with `D:\Proj` the current directory of neither panel +- **THEN** the returned effects contain a completion-read request for `D:\Proj` tagged with a request id, and the box text is unchanged +- **AND** `core::update` made no filesystem call + +### Requirement: Slow storage never blocks the event loop + +These operations SHALL run on worker threads, never on the event-loop thread, so that a slow, removable, or unreachable volume cannot freeze input or redraw: + +- the path-box target check and the path-box completion read; +- opening a file in the F3 viewer; +- opening a file in the built-in editor; +- saving from the built-in editor; +- launching a file's associated application; +- writing the applied theme and the panel split to `config.toml`. + +While an editor or viewer open is outstanding, the status line of the panel it was requested from SHALL show `Opening …`, and the panels SHALL keep accepting input. A newer F3 or F4 request SHALL supersede an outstanding open. An open reply that arrives after the user has left the panels, or after a menu, dialog, or overlay has opened, SHALL be discarded, and a viewer reply's file handle SHALL be released. + +While a save is outstanding, the editor SHALL show a `Saving…` notice on the row directly above its F-key bar, leaving the header and F-key bar unchanged, and SHALL NOT accept edits, caret moves, or further save or quit commands until the save completes or fails. The screen SHALL keep redrawing meanwhile. + +Configuration writes SHALL be performed one at a time, in the order they were requested, by a single background writer, so that two rewrites of `config.toml` never interleave. A newer value for a setting SHALL replace an older value for the same setting that has not been written yet, so the value on disk after the writes complete is always the last one requested. Moving a write to the background writer SHALL NOT change how it is written: it SHALL remain the atomic write that theme-selection "Applied theme persists to configuration" and panel-split "Split persistence to configuration" require, made durable as configuration-files "Persisted configuration writes are durable" specifies where that requirement exists, and a failed write SHALL NOT interrupt the session. When the application exits, by quit or by any other exit path, the system SHALL first complete every configuration write still outstanding, waiting at most about 2 seconds. History persistence (`history.json`) is outside this rule. + +On Windows, the worker that launches the associated application SHALL initialize COM in a single-threaded apartment (`COINIT_APARTMENTTHREADED`) before calling the shell launch API and SHALL uninitialize it afterwards. A launch failure SHALL be reported on the status line of the panel the Open action was invoked on. + +#### Scenario: F4 on a slow share keeps the panels live + +- **WHEN** the user presses F4 on `notes.txt` in a panel showing `\\nas\share` and reading the file takes several seconds +- **THEN** the panels keep redrawing and moving the cursor in response to keys while the panel's status line shows `Opening notes.txt…` +- **AND** the built-in editor opens on `notes.txt` once the read completes + +#### Scenario: F3 on a file whose server stopped responding does not freeze the UI + +- **WHEN** the user presses F3 on a file on a share whose server has stopped responding +- **THEN** the UI keeps redrawing and accepting input while the open is outstanding +- **AND** when the open finally fails, the panel's status line shows the failure and the viewer does not open + +#### Scenario: Saving to a slow share shows progress and accepts no edits + +- **WHEN** the user presses F2 in the built-in editor on a file whose write takes several seconds +- **THEN** the editor shows `Saving…` above its F-key bar, the screen keeps redrawing, and typed characters are not inserted into the buffer +- **AND** when the write completes, `Saving…` disappears, the modified indicator clears, and typing edits the buffer again + +#### Scenario: A newer F3 supersedes an outstanding open + +- **WHEN** the user presses F3 on `slow.log`, moves to `fast.txt`, and presses F3 again before `slow.log` has opened +- **THEN** the viewer opens `fast.txt`, and the late answer for `slow.log` is discarded without replacing it + +#### Scenario: A late open does not pull the user out of a menu + +- **WHEN** an F4 open is outstanding and the user opens the F9 menu before it completes +- **THEN** the menu stays open, the editor does not open when the read completes, and the late result is discarded + +#### Scenario: Launching a slow handler leaves the panels interactive + +- **WHEN** the user activates Open on a document whose associated application takes several seconds to start +- **THEN** the panels keep redrawing and responding to keys during the launch +- **AND** if the launch fails, the failure is shown on the status line of the panel the Open action was invoked on, even if the user has switched panels since + +#### Scenario: COM is initialized on the launch worker + +- **WHEN** Open launches a file on Windows +- **THEN** the shell launch API is called on a worker thread that initialized COM in a single-threaded apartment beforehand, and that thread uninitializes COM after the call returns + +#### Scenario: Applying a theme on a slow profile directory keeps the UI live + +- **WHEN** the profile directory is on a network share that takes several seconds to answer and the user applies a theme from the theme picker +- **THEN** the new theme is shown and the UI keeps redrawing and accepting keys while `config.toml` is being rewritten in the background + +#### Scenario: Rapid split adjustments persist the final value + +- **WHEN** the split is at 50/50 on a 100-column terminal and the user presses Ctrl+Right eight times in quick succession, ending at a 66% split +- **THEN** the input is never held up by the writes, and once the writer has caught up `config.toml` contains `panel_split = 66` + +#### Scenario: A theme and a split change made together are both saved + +- **WHEN** the user applies a theme and, before that write finishes, adjusts the split +- **THEN** after both writes complete `config.toml` contains both the new `theme` and the new `panel_split` + +#### Scenario: A setting changed just before quit is saved + +- **WHEN** the user adjusts the split and immediately quits and confirms, on a responsive disk +- **THEN** the process exits only after `config.toml` holds the new `panel_split` value + +#### Scenario: An unresponsive profile directory cannot hold quit open + +- **WHEN** a configuration write is blocked on a profile directory that has stopped answering and the user confirms quit +- **THEN** the application exits after about 2 seconds, and `config.toml` is left with either its complete previous content or its complete new content + +### Requirement: Modal overlays are mutually exclusive by construction + +The system SHALL hold the state of the modal overlays in a single overlay slot in `filecommand-core`'s `State`, so that at most one of them is open at any time. The modal overlays are the F9 menu, the drive-select dialog, fuzzy jump, find file, the F2 user menu, the theme picker, the file-action menu, the Help window (with its nested About dialog), and the startup warning. Each overlay's handler SHALL receive that overlay's state directly from the slot, so no handler depends on a separately checked gate and none can run while its overlay is closed. + +The quit-confirmation dialog is not part of the slot. It SHALL remain a separate layer above every other context, as "Quit request keys and confirmation" requires. The file-operation dialogs, the viewer, and the editor SHALL remain phases. + +Which layer owns input SHALL be defined once, by a single precedence order in `filecommand-core`, and the reducer's command routing, the TUI's key routing, its mouse routing, and its rendering and hit-map construction SHALL all follow it. The order, highest first: +1. the quit confirmation; +2. a file-operation dialog; +3. the viewer; +4. the editor; +5. the open overlay; +6. the panels or the startup splash. + +A layer SHALL handle only the commands it claims; any other command, including worker replies, SHALL pass to the layers below it as it does today. While the terminal is below the 60x16 floor, input routing SHALL additionally follow "Resize handling with 60x16 hard floor and placeholder". + +This structure SHALL NOT change which overlay is shown, which keys or clicks it accepts, or how it renders, in any state the application can reach. + +#### Scenario: A menu item opens its dialog in place of the menu + +- **WHEN** the F9 menu is open and the user activates Commands → Find file +- **THEN** the menu closes and the find-file dialog opens +- **AND** only the find-file dialog is shown, and it receives the next key + +#### Scenario: The user menu's Themes entry replaces the user menu + +- **WHEN** the F2 user menu is open with its built-in Themes entry highlighted and the user presses Enter +- **THEN** the user menu closes and the theme picker opens in its place, with the active theme highlighted + +#### Scenario: Another overlay's shortcut is ignored while an overlay is open + +- **WHEN** the fuzzy-jump dialog is open and the user presses F9 or Alt+F1 +- **THEN** neither the menu nor the drive-select dialog opens +- **AND** the fuzzy-jump dialog stays open with its pattern and highlight unchanged + +#### Scenario: Unbound keys never reach the panels or the command line + +- **WHEN** the F2 user menu is open and the user types a printable character that the user menu does not bind +- **THEN** the key is ignored, so the path boxes and the panel cursor are unchanged + +#### Scenario: Late results of a dismissed search touch no other overlay + +- **WHEN** the user dismisses the find-file dialog with Esc while its search is still running, then opens the Help window +- **AND** further matches from the dismissed search arrive +- **THEN** the results are discarded, the Help window is unchanged, and no find-file dialog reappears + +#### Scenario: An overlay command with its overlay closed does nothing + +- **WHEN** a command that only an overlay binds (for example confirming fuzzy jump) reaches the reducer while that overlay is not open +- **THEN** the state is unchanged, no effect is returned, and nothing panics + +#### Scenario: The quit dialog stacks above an open overlay + +- **WHEN** the quit-confirmation dialog is open above an open F9 pull-down and the user cancels it +- **THEN** the pull-down is still open exactly as before, and it receives the next key + +#### Scenario: Worker replies pass beneath the quit dialog + +- **WHEN** a copy job is running, the quit-confirmation dialog is open above its progress dialog, and the job reports progress +- **THEN** the progress dialog underneath shows the new progress and the quit-confirmation dialog stays open + +#### Scenario: One precedence order everywhere + +- **WHEN** the routing of a key, the routing of a mouse event, and the drawing and hit map of a frame are each computed for the same state +- **THEN** all of them treat the same layer as the one that owns input, the layer defined by the single precedence order diff --git a/openspec/changes/path-box/specs/command-line/spec.md b/openspec/changes/path-box/specs/command-line/spec.md new file mode 100644 index 0000000..5a68444 --- /dev/null +++ b/openspec/changes/path-box/specs/command-line/spec.md @@ -0,0 +1,51 @@ +## REMOVED Requirements + +The `command-line` capability is retired by this change. Every requirement below is removed; archiving deletes `openspec/specs/command-line/`. Ctrl+O and the suspended-TUI shell run live on under `application-shell`. + +### Requirement: Command-line prompt and printable-key routing +**Reason**: The command line is replaced by two path boxes with dedicated focus; printable keys over the panels no longer have a sink. +**Migration**: `path-box` "Two path boxes replace the command line", "Focusing a path box", "Panel keys are independent of the path boxes". + +### Requirement: Run command via shell in suspended-TUI mode +**Reason**: Behaviour unchanged; it only moves home because the capability is retired. +**Migration**: `application-shell` "Shell commands run only from the Run entry and the user menu" and "A suspended shell run restores the terminal". + +### Requirement: Command history navigation +**Reason**: No command is ever run, so there is no command history. Up/Down always move the panel cursor. +**Migration**: None. `history.json` keeps its `commands` array for file compatibility; it is preserved on write and never appended to. + +### Requirement: Paste filename and path to command line +**Reason**: There is no command line to paste into. +**Migration**: None. Ctrl+Enter and Ctrl+] are unbound; `key.paste_name` and `key.paste_path` are no longer recognised in `config.toml`. Clipboard export of paths and names (`clipboard-export`) is unchanged. + +### Requirement: Panels on/off reveals terminal scrollback +**Reason**: Behaviour unchanged; it only moves home because the capability is retired. +**Migration**: `application-shell` "Panels on/off reveals terminal scrollback". + +### Requirement: Configurable shell with documented latency tradeoff +**Reason**: Behaviour unchanged; it only moves home because the capability is retired. +**Migration**: `application-shell` "Configurable shell with documented latency tradeoff". + +### Requirement: Command-line builtin whitelist +**Reason**: Nothing is typed to run. +**Migration**: None. + +### Requirement: cd navigates the active panel or rejects a nonexistent target +**Reason**: Replaced by the path box, which reaches any folder with completion and lands the cursor on the entry. +**Migration**: `path-box` "Enter resolves the path box text", "Enter selects the named entry", "Enter on the panel's own directory or a root", "A listed parent answers the existence check", "The existence check never blocks the event loop". + +### Requirement: del and rmdir route into the existing delete-confirmation flow +**Reason**: Typed delete verbs are removed. +**Migration**: F8, the Delete key and the file-action menu's Delete entry open the same delete-confirmation flow. + +### Requirement: Clear the command line with a dedicated key +**Reason**: There is no command line to clear; Esc resets a focused path box. +**Migration**: None. Ctrl+Y is unbound; `key.clear_line` is no longer recognised in `config.toml`. + +### Requirement: Command history dialog (Alt+F8) +**Reason**: There is no command history. +**Migration**: None. Alt+F8 is unbound and Commands → History is removed (`pulldown-menus` "Menu contents"). + +### Requirement: cd, del and rmdir targets resolve to a normalized absolute path +**Reason**: The verbs are removed; the resolution rules themselves are owned by `operation-dialogs`. +**Migration**: `path-box` "Enter resolves the path box text" resolves box text by operation-dialogs "Relative destinations resolve against the source panel directory". diff --git a/openspec/changes/path-box/specs/file-action-menu/spec.md b/openspec/changes/path-box/specs/file-action-menu/spec.md new file mode 100644 index 0000000..d52d657 --- /dev/null +++ b/openspec/changes/path-box/specs/file-action-menu/spec.md @@ -0,0 +1,62 @@ +## MODIFIED Requirements + +### Requirement: Enter on a file opens the action menu + +When the user presses Enter with the active panel's cursor on a file entry, the system SHALL open a modal file-action menu targeting that entry. The menu SHALL target the cursor entry only — it SHALL NOT consume or alter the multi-entry selection. Enter on a directory or the `..` entry SHALL continue to navigate and is not affected by this menu. Menu state SHALL live in `filecommand-core` with all mutations flowing through `core::update`. + +#### Scenario: Enter on a non-executable file opens the menu + +- **WHEN** the cursor is on `notes.txt` and the user presses Enter +- **THEN** the file-action menu opens as a modal dialog targeting `notes.txt` + +#### Scenario: Selection is not consumed + +- **WHEN** 5 entries are selected, the cursor is on `report.txt`, and the user presses Enter +- **THEN** the menu opens targeting only `report.txt` +- **AND** the 5-entry selection remains intact while the menu is open and after it closes + +#### Scenario: Non-empty command buffer takes precedence + +- **WHEN** a path box has focus, reads `C:\NORTON\dir`, the panel cursor is on a file and the user presses Enter +- **THEN** Enter is handled by the path box (path-box "Enter selects the named entry") and no file-action menu opens + +#### Scenario: Enter on a directory still navigates + +- **WHEN** the cursor is on a subdirectory and the user presses Enter +- **THEN** the panel navigates into that directory and no file-action menu opens + +### Requirement: Menu actions route to existing flows + +Each file-action menu entry SHALL route into the existing capability flow for that action, applied to the menu's target entry: Open SHALL launch the target through the platform file association per "Open with the associated application"; View SHALL open the F3 viewer; Edit SHALL follow the F4 edit path (external editor when configured, otherwise the built-in editor rules); Copy SHALL open the F5 destination-input dialog pre-filled with the opposite panel's path, scoped to the single target entry; Move SHALL open the F6 destination-input dialog pre-filled with the opposite panel's path, scoped to the single target entry; Delete SHALL open the F8 delete-confirmation flow for the single target entry; Send to clipboard SHALL run the `clipboard-export` Files action scoped to the single target entry; Run (executables only) SHALL use the existing suspended-TUI spawn path. Downstream behavior — overwrite conflicts, progress, error recovery, skipped-files summary, and panel re-read on completion — SHALL follow the existing `file-operations` and `operation-dialogs` requirements unchanged. + +#### Scenario: Open launches the associated application + +- **WHEN** the user activates Open for `report.pdf` +- **THEN** the menu closes, `report.pdf` is handed to the platform file association, and the panels remain interactive without any TUI suspension + +#### Scenario: View opens the viewer + +- **WHEN** the user activates View for `notes.txt` +- **THEN** the menu closes and the F3 viewer opens on `notes.txt` + +#### Scenario: Copy opens the destination dialog for the single entry + +- **WHEN** the left panel is active, the right panel shows `D:\backup`, and the user activates Copy for `report.txt` +- **THEN** the menu closes and the destination-input dialog opens pre-filled with `D:\backup` +- **AND** accepting it starts a copy job whose scope is exactly `report.txt` + +#### Scenario: Delete requires the existing confirmation + +- **WHEN** the user activates Delete for `notes.txt` +- **THEN** the menu closes and the delete-confirmation dialog names `notes.txt` and states that deletion is permanent +- **AND** declining the confirmation deletes nothing + +#### Scenario: Send to clipboard copies the single entry + +- **WHEN** the user activates Send to clipboard for `notes.txt` +- **THEN** the menu closes and the clipboard holds a file object for exactly `notes.txt` + +#### Scenario: Run spawns via the suspended-TUI path + +- **WHEN** the user activates Run for `build.bat` +- **THEN** the menu closes and `build.bat` runs via the suspended-TUI spawn path in the panel's current directory diff --git a/openspec/changes/path-box/specs/filesystem-error-handling/spec.md b/openspec/changes/path-box/specs/filesystem-error-handling/spec.md new file mode 100644 index 0000000..b004da3 --- /dev/null +++ b/openspec/changes/path-box/specs/filesystem-error-handling/spec.md @@ -0,0 +1,43 @@ +## MODIFIED Requirements + +### Requirement: Error messages name the affected path + +A user-facing filesystem error SHALL identify the file or directory it concerns together with the reason for the failure. A bare operating-system message, such as "Access is denied. (os error 5)", SHALL NOT be shown on its own. + +The status-line messages that are built from an operating-system error SHALL name the item by its file or directory name; the panel's title already shows the directory it lives in. They SHALL take these forms: + +- a panel whose directory cannot be read SHALL show `Cannot read : ` in its read-error state; +- a failure to open a file in the F3 viewer or the built-in editor SHALL read `Cannot open : `; +- a failed built-in editor save SHALL read `Cannot save : `, where the reason is the operating system's message or the editor's own explanation of why it refused to save. + +Status-line messages that already name their target, such as the path box's not-found rejection and the Open-with-associated-application failure, keep their existing text. + +The multi-line job dialogs SHALL show the full path: + +- the error-recovery dialog SHALL show the full path of the failing item above the operating-system message; +- each row of the end-of-job summary SHALL show the item's full path followed by its reason. + +#### Scenario: Access-denied listing names the directory + +- **WHEN** a panel navigates to `C:\Windows\CSC` and the read fails with access denied +- **THEN** the panel's read-error message reads `Cannot read CSC: Access is denied. (os error 5)`, and the panel title shows `C:\Windows\CSC` + +#### Scenario: Viewer open failure names the file + +- **WHEN** F3 is pressed on `locked.db` and the file cannot be opened because another process holds it exclusively +- **THEN** the panel's status line reads `Cannot open locked.db: ` followed by the operating system's sharing-violation message + +#### Scenario: Editor save failure names the file + +- **WHEN** F2 in the built-in editor fails to write `notes.txt` because the disk is full +- **THEN** the editor reports `Cannot save notes.txt: ` followed by the operating system's message, and the buffer stays modified + +#### Scenario: The error-recovery dialog shows the full path + +- **WHEN** a copy job fails to read `D:\data\reports\q3.xlsx` with a permission error +- **THEN** the error-recovery dialog shows `D:\data\reports\q3.xlsx` on one line and the operating system's message on the next + +#### Scenario: Summary rows show the full path and reason + +- **WHEN** a job ends having skipped `D:\data\old.log` because the target already exists +- **THEN** the end-of-job summary row reads `D:\data\old.log: target already exists` diff --git a/openspec/changes/path-box/specs/help-and-about/spec.md b/openspec/changes/path-box/specs/help-and-about/spec.md new file mode 100644 index 0000000..31164b2 --- /dev/null +++ b/openspec/changes/path-box/specs/help-and-about/spec.md @@ -0,0 +1,30 @@ +## MODIFIED Requirements + +### Requirement: Help topic list + +The system SHALL render, below the identity header, a scrollable list of help topics whose first entry is `About FileCommand`, highlighted (white on black) as the initial cursor position, followed by the v1 topics `Keyboard reference`, `Mouse`, `Panels and display modes`, `File operations`, `Menus`, `Viewer`, `Editor`, `Path box`, `Modern extras`, and `Configuration`, and it SHALL show `↑` / `↓` scroll arrows on the right border only when the list overflows the visible area. + +#### Scenario: List opens with About FileCommand highlighted first + +- **WHEN** the Help window first appears +- **THEN** the topic list is shown with `About FileCommand` as the first entry and the cursor highlight (white on black) resting on it + +#### Scenario: Cursor moves through the topic list + +- **WHEN** the user presses Down arrow one or more times within the topic list +- **THEN** the white-on-black highlight moves to the next topic and the list scrolls as needed to keep the highlighted topic visible + +#### Scenario: Scroll arrows appear only on overflow + +- **WHEN** the topic list is taller than the visible list area +- **THEN** `↑` / `↓` scroll arrows render on the window's right border, and they are absent when the entire list fits + +#### Scenario: Mouse topic + +- **WHEN** the user opens the `Mouse` topic +- **THEN** its page documents click, double-click, wheel, right-click, Ctrl+click, Shift+drag for native text selection, and the `[mouse]` / `--nomouse` off switches + +#### Scenario: Path box topic + +- **WHEN** the user opens the `Path box` topic +- **THEN** its page documents the focus key (Ctrl+G, `key.focus_path`), Tab / Shift+Tab completion, Enter selection, Esc, and that no command line or shell builtins exist diff --git a/openspec/changes/path-box/specs/panel-navigation/spec.md b/openspec/changes/path-box/specs/panel-navigation/spec.md new file mode 100644 index 0000000..5f37d69 --- /dev/null +++ b/openspec/changes/path-box/specs/panel-navigation/spec.md @@ -0,0 +1,75 @@ +## MODIFIED Requirements + +### Requirement: Parent-directory navigation + +The system SHALL navigate the active panel to its parent directory when Ctrl+PgUp is pressed, and when Backspace is pressed while no path box has focus. When the current directory is already a filesystem root with no parent, the navigation SHALL be a no-op. + +#### Scenario: Ctrl+PgUp goes to the parent + +- **WHEN** the active panel is in a non-root directory and Ctrl+PgUp is pressed +- **THEN** the panel SHALL navigate to its parent directory and begin listing it + +#### Scenario: Backspace on empty command line goes to the parent + +- **WHEN** no path box has focus and Backspace is pressed +- **THEN** the active panel SHALL navigate to its parent directory + +#### Scenario: Parent navigation at a root is a no-op + +- **WHEN** the active panel is at a filesystem root with no parent and a parent-navigation command is issued +- **THEN** the panel SHALL remain in the current directory unchanged + +### Requirement: Swap panels (Ctrl+U) + +The system SHALL exchange the left and right panels' complete state — directory, tabs, listing, cursor, scroll offset, selection, sort mode, filter, display mode, and git info — on Ctrl+U or Commands → Swap panels, keeping the active side and the split percentage unchanged, without re-reading either directory. A swap requested while either panel's listing is still streaming SHALL be ignored. After a swap the system SHALL re-issue each panel's pending Info-mode and git-info queries so no reply from before the swap is applied to the wrong panel. + +#### Scenario: Swap keeps focus on the same side +- **WHEN** the left panel is active showing `C:\a` and the right shows `D:\b`, and the user presses Ctrl+U +- **THEN** the left panel shows `D:\b`, the right shows `C:\a`, the left panel is still active, and the left path box reads `D:\b\` while the right reads `C:\a\` + +#### Scenario: Swap carries tabs and selection +- **WHEN** the right panel has three tabs and two selected entries and the user presses Ctrl+U +- **THEN** the left panel now has those three tabs and the same two selected entries + +#### Scenario: Swap refused while reading +- **WHEN** the right panel's listing is still streaming and the user presses Ctrl+U +- **THEN** nothing changes + +#### Scenario: Git info follows the swap +- **WHEN** the left panel is inside a git repository and the user presses Ctrl+U +- **THEN** the right panel's border shows that repository's branch suffix after its re-issued query resolves, and the left panel shows none + +### Requirement: Superseded directory listings are discarded and cancelled + +Every directory listing the system starts for a panel SHALL be tagged with a fresh request id that becomes that panel's current listing request, and every streamed batch of entries, completion notice, and failure notice SHALL carry the id of the listing that produced it. The system SHALL apply a listing result only when its id matches the panel's current listing request and SHALL silently discard every other listing result, so that a listing superseded by a navigation, a re-read, a drive change, a file-operation refresh, or any other new listing of the same panel never adds entries to, completes, or fails the listing that replaced it. When a listing is superseded while it is still being read, the system SHALL signal its reader to stop; the reader SHALL stop at its next check and SHALL send no further entries, completion notice, or failure notice. + +#### Scenario: Backspace out of a slow directory mid-listing + +- **WHEN** the active panel enters `C:\big` and, while that listing is still streaming, the user presses Backspace to return to `C:\` +- **THEN** the panel shows only the entries of `C:\` — no entry from `C:\big`, not even `C:\big`'s `..` entry — and the `Reading… N` count covers only `C:\`'s entries +- **AND** the read of `C:\big` is signalled to stop + +#### Scenario: cd elsewhere mid-listing + +- **WHEN** the panel's listing of `D:\logs` is still streaming and the user focuses the path box, types `C:\Work\notes.txt` (an existing file) and presses Enter, so the panel navigates to `C:\Work` +- **THEN** only `C:\Work`'s entries appear in the panel, and none of `D:\logs`'s entries arrive later + +#### Scenario: Superseded completion does not complete the new listing + +- **WHEN** a superseded listing's completion notice arrives after the panel has started reading a different directory +- **THEN** the mini-status keeps showing `Reading… N` for the new listing until the new listing's own completion arrives + +#### Scenario: Superseded failure does not mark the new listing failed + +- **WHEN** the read of a directory the user has already left fails with access denied after the panel started reading another directory +- **THEN** no error is shown for the current directory and its listing continues normally + +#### Scenario: Two quick re-reads do not duplicate entries + +- **WHEN** the user presses Ctrl+R twice in quick succession on a directory with 500 entries +- **THEN** after both reads finish the panel lists each of the 500 entries exactly once + +#### Scenario: Superseded reader stops + +- **WHEN** a listing is superseded while its reader is still producing entries +- **THEN** the reader stops at its next check and sends no further entries, completion notice, or failure notice diff --git a/openspec/changes/path-box/specs/path-box/spec.md b/openspec/changes/path-box/specs/path-box/spec.md new file mode 100644 index 0000000..8284c4c --- /dev/null +++ b/openspec/changes/path-box/specs/path-box/spec.md @@ -0,0 +1,265 @@ +# Spec Delta + +## Purpose + +Two path boxes under the panels show each panel's current directory, complete folder and file names on Tab, and on Enter put the panel cursor on the entry the text names. + +## ADDED Requirements + +### Requirement: Two path boxes replace the command line + +The system SHALL render, on the row directly above the F-key bar, one path box under each panel, occupying that panel's columns. An unfocused box SHALL show its panel's current directory followed by the platform path separator in the `commandline` theme role, SHALL follow the panel through navigation, tab switches and swaps, and SHALL truncate from the left with a leading `…` when wider than the box. The row SHALL show no shell prompt, and typed text SHALL never be run through a shell. + +#### Scenario: Both boxes show their panel's directory +- **WHEN** the left panel is at `C:\NORTON` and the right panel is at `D:\WORK` +- **THEN** the row above the key bar shows `C:\NORTON\` under the left panel and `D:\WORK\` under the right panel + +#### Scenario: A box follows its panel +- **WHEN** the left panel navigates into `C:\NORTON\sub` +- **THEN** the left box reads `C:\NORTON\sub\` and the right box is unchanged + +#### Scenario: Switching panels changes no box text +- **WHEN** the user presses Tab to make the other panel active +- **THEN** both boxes keep showing their own panel's directory + +#### Scenario: A long directory truncates from the left +- **WHEN** a panel's directory is wider than its box +- **THEN** the box shows a leading `…` followed by the tail of the path, and nothing paints outside the box + +### Requirement: Focusing a path box + +The system SHALL give keyboard focus to the active panel's path box on the focus-path chord (default Ctrl+G, overridable via `key.focus_path`) while the panels own input, and to either box when it is clicked; a click SHALL also make that box's panel active. Focusing SHALL end an active type-ahead search. On focus the box SHALL hold the panel's directory plus separator with the caret at the end, own the terminal cursor, and render in the dialog text-field style. + +#### Scenario: Ctrl+G focuses the active panel's box +- **WHEN** the right panel is active at `D:\WORK` and the user presses Ctrl+G +- **THEN** the right box has focus, reads `D:\WORK\` with the caret at the end, and the terminal cursor is in it + +#### Scenario: Clicking the other box activates that panel +- **WHEN** the left panel is active and the user clicks the right path box +- **THEN** the right panel becomes active and the right box has focus + +#### Scenario: Focusing ends type-ahead +- **WHEN** type-ahead is active with pattern `re` and the user presses Ctrl+G +- **THEN** type-ahead ends, its pattern is gone from the mini-status, and the box has focus + +#### Scenario: Rebinding the focus key +- **WHEN** `config.toml` sets `key.focus_path = "ctrl+e"` +- **THEN** Ctrl+E focuses the box and Ctrl+G no longer does + +### Requirement: A focused path box owns the keyboard + +While a box has focus it SHALL own every key: keys it does not bind SHALL be ignored and SHALL NOT reach the panels, menus or F-key actions. The box SHALL lose focus and reset to the panel's directory when a menu, dialog or overlay opens for any other reason, or when the user clicks outside it; the click is then handled as it would be with no box focused. + +#### Scenario: Unbound keys stay inside the box +- **WHEN** a box has focus and the user presses F9, then Alt+F1, then Ctrl+U +- **THEN** no menu or dialog opens, the panels are not swapped, and the box keeps focus and text + +#### Scenario: A click outside the box cancels it +- **WHEN** a box has focus with edited text and the user clicks an entry in the left panel +- **THEN** the box closes showing the panel's directory and the clicked entry receives the panel cursor + +#### Scenario: A dialog opening for another reason closes the box +- **WHEN** a box has focus and the startup-warning dialog or a file-operation dialog opens +- **THEN** the box closes and resets, and the dialog receives the next key + +### Requirement: Panel keys are independent of the path boxes + +While no box has focus the panels SHALL keep their full key map regardless of any box text: Tab switches panels, Backspace goes to the parent, Up/Down move the cursor, Left/Right are unbound, `+`/`-`/`*` open group selection and invert selection, and a plain printable key over the panels SHALL be ignored. + +#### Scenario: Panel keys keep their meaning without focus +- **WHEN** no box has focus and the user presses Tab, then Backspace, then `+` +- **THEN** the active panel switches, the newly active panel goes to its parent, and the group-select dialog opens; no box text changes + +#### Scenario: A printable key over the panels is ignored +- **WHEN** no box has focus and the user types `d` +- **THEN** nothing changes: no box text, no cursor movement, no search + +#### Scenario: Up and Down always move the cursor +- **WHEN** no box has focus and the user presses Up +- **THEN** the panel cursor moves up one row + +### Requirement: Editing inside a focused path box + +A focused path box SHALL be backed by the shared line-edit model (text-field-editing "Caret model and editing keys") and SHALL bind its full key set — printable insert, Backspace, Delete, Left/Right, Home/End, Ctrl+Left/Ctrl+Right, Ctrl+Backspace/Ctrl+Delete — plus Ctrl+V and Shift+Ins paste (text-field-editing "Paste into a text field") and a terminal paste (text-field-editing "Terminal paste arrives as one paste"). The text SHALL scroll horizontally so the caret stays visible. + +#### Scenario: Edit mid-path +- **WHEN** the box reads `C:\NORTON\` and the user presses Home, Delete three times and types `D:\` +- **THEN** the box reads `D:\NORTON\` with the caret after the first `\` + +#### Scenario: Paste into the box +- **WHEN** the box has focus with the caret at the end of `C:\` and the clipboard holds `Program Files` +- **THEN** after Ctrl+V the box reads `C:\Program Files` + +#### Scenario: Long text keeps the caret visible +- **WHEN** the box text is wider than the box and the caret is at the end +- **THEN** the visible window scrolls so the caret remains on-screen + +### Requirement: Tab completes the last path segment + +While a box has focus, Tab SHALL complete the segment after the last path separator: the text before it names a folder, resolved against the panel directory as in "Enter resolves the path box text", and the text after it is a case-insensitive prefix. Tab SHALL replace that segment with a candidate's on-disk name, and a directory candidate SHALL get a trailing separator. + +#### Scenario: Tab completes from the panel listing +- **WHEN** the active panel at `C:\NORTON` lists `docs`, `Downloads` and `data.txt` among others, the box reads `C:\NORTON\d` and the user presses Tab +- **THEN** the box reads `C:\NORTON\docs\` (directories come first) and no completion-read request is emitted + +#### Scenario: The on-disk name replaces the typed prefix +- **WHEN** the box reads `C:\NORTON\READ` and the folder holds `readme.md` +- **THEN** Tab makes the box read `C:\NORTON\readme.md` + +#### Scenario: Relative and parent-relative folders complete +- **WHEN** the active panel is at `C:\work\proj`, the box reads `..\x` and `C:\work\xray` exists +- **THEN** Tab makes the box read `..\xray\` + +### Requirement: Completion candidates and cycling order + +The candidates SHALL be every child of the folder whose name starts with the prefix, directories first then files, each in case-insensitive name order. Tab SHALL move to the next candidate and Shift+Tab to the previous one; both SHALL wrap. + +#### Scenario: Tab cycles and wraps, Shift+Tab reverses +- **WHEN** the candidates are `docs`, `Downloads` and `data.txt`, the box reads `C:\NORTON\docs\` after a first Tab, and the user presses Tab, Tab, Tab, then Shift+Tab +- **THEN** the text goes `C:\NORTON\Downloads\`, `C:\NORTON\data.txt`, `C:\NORTON\docs\`, then `C:\NORTON\data.txt` + +#### Scenario: Matching ignores case +- **WHEN** the folder holds `Readme.md` and `recipes` and the box reads `C:\NORTON\RE` +- **THEN** Tab offers `recipes\` first (a directory) and then `Readme.md` + +### Requirement: Completion cycle and mini-status + +The prefix in force SHALL be the one present when the cycle started, so repeated Tab presses cycle rather than narrow; any edit other than Tab or Shift+Tab SHALL end the cycle. While a cycle is active the box's panel mini-status SHALL show `k/n matches`; when no child matches it SHALL show `no matches` and Tab SHALL leave the text unchanged. + +#### Scenario: Mini-status follows the cycle +- **WHEN** three children match and the user presses Tab three times +- **THEN** the mini-status shows `1/3 matches`, `2/3 matches`, then `3/3 matches` + +#### Scenario: An edit ends the cycle +- **WHEN** a cycle is active on `C:\NORTON\docs\` and the user presses Backspace +- **THEN** the mini-status no longer shows a match count, and the next Tab starts a new cycle for the new prefix + +#### Scenario: No match +- **WHEN** the box reads `C:\NORTON\zz` and no child of `C:\NORTON` starts with `zz` +- **THEN** Tab leaves the text unchanged and the mini-status shows `no matches` + +### Requirement: Completion candidates come from a panel listing when possible + +Children SHALL be obtained without filesystem I/O in the reducer: when the folder is the current directory of a panel whose listing is complete, that listing SHALL answer at once and no completion read SHALL be requested. + +#### Scenario: The other panel's listing answers +- **WHEN** the left box reads `D:\WORK\re`, the right panel shows `D:\WORK` with a complete listing, and the user presses Tab +- **THEN** the candidates come from the right panel's listing and no completion-read request is emitted + +#### Scenario: A streaming listing is not used +- **WHEN** the folder is a panel's current directory but that listing is still streaming +- **THEN** Tab requests a completion read rather than answering from the partial listing + +### Requirement: Completion reads run off the event loop + +When no complete panel listing covers the folder, the reducer SHALL emit a completion-read request tagged with a request id, run on a worker thread through the `\\?\` long-path abstraction, with the mini-status showing `Reading…` until the reply arrives; Tab meanwhile SHALL be ignored. A reply SHALL be applied only when its id is outstanding and its folder is the one the text names. Names SHALL be reused while the folder part of the text is unchanged. + +#### Scenario: A folder outside the panels is read on a worker +- **WHEN** the box reads `D:\Proj\re` and `D:\Proj` is the current directory of neither panel +- **THEN** Tab emits a completion-read request for `D:\Proj` with a request id, the mini-status shows `Reading…`, and the box text is unchanged +- **AND** when the reply lists `README.md`, `release` and `src`, the box reads `D:\Proj\release\` and the mini-status shows `1/2 matches` + +#### Scenario: A stale reply is discarded +- **WHEN** a completion read for `D:\Proj` is outstanding and the user edits the box to `E:\x` before it answers +- **THEN** the reply changes nothing and the mini-status does not show matches for `D:\Proj` + +#### Scenario: Cached names survive edits within the same folder +- **WHEN** the reply for `D:\Proj` has been applied and the user presses Backspace until the box reads `D:\Proj\s`, then Tab +- **THEN** the box reads `D:\Proj\src\` without a second completion-read request + +#### Scenario: Unreadable folder +- **WHEN** the completion read for `\\offline\share` fails +- **THEN** the mini-status shows `no matches`, Tab leaves the text unchanged, and the UI never stopped accepting keys + +### Requirement: Enter resolves the path box text + +Enter in a focused box SHALL resolve the text, ignoring one trailing separator, to a normalized absolute path by the rules of operation-dialogs "Relative destinations resolve against the source panel directory", against the box's panel directory, and SHALL verify that an entry of that name exists in its parent. When no such entry exists, or the parent cannot be read, the panel's error state SHALL name the path and the box SHALL keep focus and text. + +#### Scenario: A relative path resolves against the panel +- **WHEN** the active panel is at `C:\work\proj`, `C:\work\old.txt` exists and the box reads `..\old.txt` +- **THEN** the panel lists `C:\work` with the cursor on `old.txt` + +#### Scenario: A drive-relative path resolves like a destination +- **WHEN** the active panel is at `D:\work`, `D:\work\old` exists and the box reads `D:old` +- **THEN** the panel stays at `D:\work` with the cursor on `old` + +#### Scenario: A missing path keeps the box open +- **WHEN** the box reads `C:\NORTON\nosuch.txt` and no such entry exists +- **THEN** the panel's directory is unchanged, the panel shows an error naming `C:\NORTON\nosuch.txt`, and the box keeps focus with its text + +### Requirement: Enter selects the named entry + +When the entry exists, the box's panel SHALL navigate to the entry's parent directory unless it is already there, SHALL place the cursor on the entry (matching the exact name first, else case-insensitively), and the box SHALL close with focus returning to the panel. The panel SHALL NOT enter a directory the text names; a directory is selected in its parent. + +#### Scenario: Enter on a listed file lands the cursor +- **WHEN** the active panel is at `C:\NORTON`, lists `readme.md`, the box reads `C:\NORTON\readme.md` and the user presses Enter +- **THEN** the box closes, the panel is still at `C:\NORTON`, and the cursor is on `readme.md` + +#### Scenario: Enter on a folder selects it in its parent +- **WHEN** the active panel is at `C:\NORTON`, the box reads `C:\NORTON\docs\` and the user presses Enter +- **THEN** the panel stays at `C:\NORTON` with the cursor on `docs`; it does not enter `docs` + +#### Scenario: Enter on a path elsewhere navigates to the parent and lands +- **WHEN** the active panel is at `C:\NORTON`, the box reads `D:\Proj\src\main.rs`, which exists, and the user presses Enter +- **THEN** once the check succeeds the panel lists `D:\Proj\src` with the cursor on `main.rs` and the box is closed + +#### Scenario: Case-insensitive landing +- **WHEN** the box reads `C:\NORTON\README.MD` and the listing holds `readme.md` +- **THEN** the cursor lands on `readme.md` + +### Requirement: Enter on the panel's own directory or a root + +If the resolved path equals the box's panel current directory, Enter SHALL close the box with no other change. If the resolved path is a filesystem root with no parent, the panel SHALL navigate into it and the box SHALL close. + +#### Scenario: Enter on the panel's own directory just closes the box +- **WHEN** the box reads `C:\NORTON\` unchanged since focus and the user presses Enter +- **THEN** the box closes, and the panel's directory and cursor are unchanged + +#### Scenario: A drive root is entered +- **WHEN** the box reads `D:\` and `D:` exists +- **THEN** the panel navigates to `D:\` and the box closes + +#### Scenario: A UNC share root is entered +- **WHEN** the box reads `\\server\share\` and the share exists +- **THEN** the panel navigates to `\\server\share` and the box closes + +### Requirement: A listed parent answers the existence check + +When the parent is the current directory of a panel whose listing is complete, that listing SHALL answer the existence check, matching the name case-insensitively, with no filesystem I/O in the reducer. + +#### Scenario: A listed entry needs no filesystem check +- **WHEN** the active panel at `C:\NORTON` lists `sub` and the box reads `C:\NORTON\sub` when Enter is pressed +- **THEN** the cursor lands on `sub` in response to the Enter itself, without any filesystem check + +#### Scenario: The other panel's listing answers the check +- **WHEN** the left box reads `D:\WORK\notes.txt`, the right panel shows `D:\WORK` with `notes.txt` listed, and the user presses Enter +- **THEN** the left panel navigates to `D:\WORK` with the cursor on `notes.txt` and no path-probe request is emitted + +### Requirement: The existence check never blocks the event loop + +When no complete panel listing covers the parent, the check SHALL run on a worker thread through the `\\?\` long-path abstraction, tagged with a request id, while the box's panel status line shows `Checking …`; the reply SHALL be applied only if it is the most recent check and the box still has focus with the same text, and SHALL otherwise be discarded. The panel's directory SHALL NOT change until the check succeeds. + +#### Scenario: An unlisted target is checked off-thread +- **WHEN** the box reads `\\slow\share\x.txt`, which is listed in neither panel, and the user presses Enter +- **THEN** the UI keeps redrawing and accepting keys while the panel's status line shows `Checking \\slow\share\x.txt…`, and the panel's directory is unchanged +- **AND** when the check succeeds the panel lists `\\slow\share` with the cursor on `x.txt` + +#### Scenario: A late reply is discarded +- **WHEN** a check for `\\slow\share\x.txt` is outstanding and the user presses Esc before it answers +- **THEN** the reply neither navigates nor sets an error + +#### Scenario: Reducer makes no filesystem call +- **WHEN** a unit test applies Enter for `D:\Proj\src\main.rs` with neither panel at `D:\Proj\src` +- **THEN** the returned effects contain a path-probe request for that path with a request id, the panel's `cwd` is unchanged, and the result is the same whether or not the file exists on the test machine + +### Requirement: Esc cancels the path box + +While a box has focus, Esc SHALL close it, reset its text to the panel's current directory followed by the separator, discard any completion cycle or outstanding check, and return focus to the panel without opening the quit-confirmation dialog. + +#### Scenario: Esc reverts the box +- **WHEN** the box reads `C:\NORTON\dra` with a cycle active and the user presses Esc +- **THEN** the box shows `C:\NORTON\`, no dialog opens, and the next Esc requests quit as usual + +#### Scenario: Esc discards an outstanding check +- **WHEN** a `Checking …` note is showing for the box's panel and the user presses Esc +- **THEN** the note clears, the box closes, and the check's later reply changes nothing diff --git a/openspec/changes/path-box/specs/pulldown-menus/spec.md b/openspec/changes/path-box/specs/pulldown-menus/spec.md new file mode 100644 index 0000000..76dbcac --- /dev/null +++ b/openspec/changes/path-box/specs/pulldown-menus/spec.md @@ -0,0 +1,25 @@ +## MODIFIED Requirements + +### Requirement: Menu contents + +The system SHALL populate the five menus with their defined items: Left and Right SHALL each offer Brief, Full, Tree, Quick view, Info, a separator, Name, Extension, Modif. time, Size, Unsorted, a separator, Filter, Re-read, Drive select, a separator, New tab, Close tab (mirroring each other); Files SHALL offer View, Edit, Copy, Rename/Move, Make directory, Delete, a separator, Copy to clipboard, Copy path(s), Copy name(s), a separator, Select group, Deselect group, Invert selection, a separator, Quit; Commands SHALL offer Find file, Swap panels, Panels on/off, Fuzzy jump, Menu file edit; Options SHALL offer Themes. Every listed item SHALL be enabled; the disabled-item rendering rule ("Pull-down visuals with separators and disabled items") remains available for future items but no shipped item uses it. + +#### Scenario: Files menu lists its items + +- **WHEN** the user opens the `Files` menu +- **THEN** the pull-down lists View, Edit, Copy, Rename/Move, Make directory, Delete, a separator, Copy to clipboard, Copy path(s), Copy name(s), a separator, Select group, Deselect group, Invert selection, a separator, and Quit, all enabled + +#### Scenario: Left and Right menus mirror each other + +- **WHEN** the user opens the `Left` menu and then the `Right` menu +- **THEN** both list the same display-mode, sort-mode, Filter, Re-read, Drive select, New tab, and Close tab items, all enabled + +#### Scenario: Commands and Options menus + +- **WHEN** the user opens `Commands` and then `Options` +- **THEN** Commands lists Find file, Swap panels, Panels on/off, Fuzzy jump, Menu file edit; Options lists Themes; none is disabled + +#### Scenario: Removed items are absent + +- **WHEN** the user opens every menu +- **THEN** no menu lists Attributes, Compare directories, Configuration, Editor selection, History, or Save setup diff --git a/openspec/changes/path-box/specs/quick-filter/spec.md b/openspec/changes/path-box/specs/quick-filter/spec.md new file mode 100644 index 0000000..86b1a75 --- /dev/null +++ b/openspec/changes/path-box/specs/quick-filter/spec.md @@ -0,0 +1,45 @@ +## MODIFIED Requirements + +### Requirement: Cursor and mini-status behavior under an active filter + +The system SHALL keep the panel cursor positioned on a currently visible (matching) entry while a quick filter is active. When the entry under the cursor is filtered out by a pattern change, the system SHALL move the cursor to the nearest remaining visible entry. + +When the pattern leaves no entry visible, the panel SHALL have no current entry. This can happen only where the listing has no `..` parent entry, such as a drive root. While there is no current entry, every command that acts on the entry under the cursor SHALL do nothing. That covers: + +- Enter; +- F3 and F4; +- F5, F6, F8, and the Delete key when no entries are selected; +- Ins; +- opening the file-action menu. + +No command that targets the entry under the cursor SHALL act on an entry the filter hides. Copy, Move, and Delete of selected entries are governed by selection "Operations disclose selected entries hidden by the quick filter" instead. When the pattern changes so that at least one entry is visible again, the cursor SHALL move to the nearest visible entry. + +#### Scenario: Cursor moves to a visible entry when its target is filtered out + +- **WHEN** the cursor is on an entry and the user extends the pattern so that entry no longer matches +- **THEN** the cursor moves to a still-visible matching entry rather than a hidden one + +#### Scenario: Navigation is restricted to matching entries + +- **WHEN** a quick filter is active and the user moves the cursor with the arrow keys +- **THEN** the cursor traverses only the visible matching entries (and `..`), skipping filtered-out entries + +#### Scenario: No visible entry at a drive root means no current entry + +- **WHEN** the active panel shows the root of drive `C:` (no `..` entry), the cursor is on `pagefile.sys`, and the user types a quick-filter pattern that matches no entry +- **THEN** the panel body is empty and the panel has no current entry + +#### Scenario: Cursor-targeted operations do nothing while no entry is visible + +- **WHEN** a quick filter at a drive root matches no entry, nothing is selected, and the user presses F8, F5, F6, F3, F4, or Enter +- **THEN** no dialog, viewer, editor, or menu opens, and no file is affected + +#### Scenario: Ins does nothing while no entry is visible + +- **WHEN** a quick filter at a drive root matches no entry and the user presses Ins +- **THEN** the selection set is unchanged + +#### Scenario: Widening the pattern restores a current entry + +- **WHEN** a quick filter matches no entry and the user presses Backspace until the pattern matches `notes.txt` +- **THEN** the cursor is on a visible entry and cursor-targeted commands act on that entry again diff --git a/openspec/changes/path-box/specs/responsive-layout/spec.md b/openspec/changes/path-box/specs/responsive-layout/spec.md new file mode 100644 index 0000000..4b42134 --- /dev/null +++ b/openspec/changes/path-box/specs/responsive-layout/spec.md @@ -0,0 +1,44 @@ +## MODIFIED Requirements + +### Requirement: Degraded band and per-panel breakpoints + +Between the 60×16 hard floor and the 80×24 nominal size, the system SHALL keep every feature available while degrading rendering per the rules of this capability; no surface may disappear, silently truncate mid-label, or paint outside the terminal. All panel-content degradation decisions (Full-mode column ladder, Brief column count, mini-status fields) SHALL be functions of the individual panel's width, not the terminal's, and degradation SHALL be deterministic and reversible: growing a surface back restores exactly the richer rendering it had at that size before. + +#### Scenario: All features remain available in the degraded band + +- **WHEN** the terminal is 70×20 +- **THEN** the panels, path boxes, and F-key bar all render and every command that works at 80×24 remains invocable + +#### Scenario: Degradation keys off the panel, not the terminal + +- **WHEN** the terminal is 100 columns wide with the split adjusted so the left panel is 24 columns and the right panel is 76 columns +- **THEN** the left panel renders with a reduced column set per its own width while the right panel renders all four Full-mode columns + +#### Scenario: Degradation is reversible + +- **WHEN** a panel shrinks so Full mode drops the Time column and is later restored to its prior width +- **THEN** the Time column returns and the panel renders exactly as it did at that width before + +### Requirement: Chrome degradation + +The clock SHALL render over the right panel's top border only when it fits without touching the centered path title; otherwise it SHALL be hidden entirely and never partially drawn. A focused path box SHALL scroll horizontally to keep the caret visible; an unfocused path box whose directory exceeds its width SHALL truncate from the left with a leading `…`. The panel mini-status SHALL drop the same fields in the same order as the Full-mode column ladder (Time, then Date, then Size), truncating the entry name last with `…`; the `N files selected, X bytes` summary SHALL truncate with `…` when it does not fit. + +#### Scenario: Clock hides rather than colliding + +- **WHEN** the right panel's top border is too narrow for the clock and the centered path title to both fit +- **THEN** the clock is not drawn at all and the path title renders normally + +#### Scenario: Command line keeps the caret visible + +- **WHEN** the text in a focused path box exceeds the box width +- **THEN** the visible window scrolls so the caret remains on-screen + +#### Scenario: Mini-status drops fields in ladder order + +- **WHEN** a panel is narrow enough that its mini-status cannot show name, size, date, and time +- **THEN** time is dropped first, then date, then size, and the name truncates with `…` only after all other fields are gone + +#### Scenario: Unfocused path box truncates from the left + +- **WHEN** a panel's directory is wider than its unfocused path box +- **THEN** the box shows a leading `…` followed by the tail of the path, and nothing paints outside the box diff --git a/openspec/changes/path-box/specs/startup-splash/spec.md b/openspec/changes/path-box/specs/startup-splash/spec.md new file mode 100644 index 0000000..b3a91a0 --- /dev/null +++ b/openspec/changes/path-box/specs/startup-splash/spec.md @@ -0,0 +1,21 @@ +## MODIFIED Requirements + +### Requirement: Minimum hold and key dismissal + +The system SHALL hold the splash for a minimum of 800 ms measured via the injected `Clock`, after which it is replaced by the panels even if the initial listing is still streaming. Any key press SHALL dismiss the splash immediately without waiting for the minimum hold, and the dismissing key event SHALL be consumed — never forwarded to the path boxes or panels. + +#### Scenario: Minimum hold elapses with no input + +- **WHEN** no key is pressed and 800 ms have elapsed on the injected `Clock` since the splash first painted +- **THEN** the splash is replaced by the panels +- **AND** the panels render even if the initial directory listing is still streaming + +#### Scenario: Key press dismisses before the minimum hold + +- **WHEN** a key is pressed 200 ms after the splash first painted +- **THEN** the splash is dismissed immediately, before the 800 ms minimum hold elapses + +#### Scenario: Dismissing key is consumed + +- **WHEN** a key press dismisses the splash +- **THEN** that key event is consumed and is not forwarded to the path boxes or the panels diff --git a/openspec/changes/path-box/specs/text-field-editing/spec.md b/openspec/changes/path-box/specs/text-field-editing/spec.md new file mode 100644 index 0000000..b380dd0 --- /dev/null +++ b/openspec/changes/path-box/specs/text-field-editing/spec.md @@ -0,0 +1,84 @@ +## MODIFIED Requirements + +### Requirement: Caret model and editing keys + +Every single-line text field — the Copy/Move destination and Make-directory name field, the file-action Rename field, the overwrite-conflict Rename field, the find-file pattern, the fuzzy-jump pattern, and the panels' path box — SHALL be backed by one shared line-edit model holding the text and a caret position. Printable characters SHALL be inserted at the caret; Backspace SHALL delete the character before the caret and Delete the character after it; Left/Right SHALL move the caret by one character; Home/End SHALL move it to the start/end; Ctrl+Left/Ctrl+Right SHALL move it by one word; Ctrl+Backspace/Ctrl+Delete SHALL delete the word before/after the caret. A field that opens pre-filled SHALL open with the caret at the end of the text. The model SHALL live in `filecommand-core` with all mutations flowing through `core::update`. + +#### Scenario: Insert in the middle of a pre-filled name +- **WHEN** the Rename field opens pre-filled with `draft.txt`, and the user presses Left five times, Backspace, then types `i` +- **THEN** the field reads `drift.txt` with the caret after the `i` + +#### Scenario: Home then edit a destination prefix +- **WHEN** the destination field holds `D:\backup\2026`, and the user presses Home, Delete twice, then types `\\server\share` +- **THEN** the field reads `\\server\share\backup\2026` + +#### Scenario: Word delete +- **WHEN** the field holds `C:\Users\kevin\docs` with the caret at the end and the user presses Ctrl+Backspace +- **THEN** the field reads `C:\Users\kevin\` + +#### Scenario: Pre-filled field accepts unchanged with Enter +- **WHEN** the destination field opens pre-filled with the opposite panel's path and the user presses Enter without any editing key +- **THEN** the operation starts against that path exactly as before this capability existed + +### Requirement: Paste into a text field + +The system SHALL paste the system clipboard's text at the caret of the focused dialog text field on Shift+Ins or Ctrl+V, with any line breaks replaced by a single space. When the clipboard holds no text, the key SHALL have no effect. While a path box has focus, Ctrl+V and Shift+Ins SHALL paste into it. Over the panels with no path box focused, Ctrl+V SHALL have no effect, Shift+Ins SHALL keep its selection-toggle meaning, and Ctrl+Shift+Ins SHALL remain the clipboard-paths action. + +#### Scenario: Paste a path into the destination field +- **WHEN** the clipboard holds `E:\archive` and the destination field is focused with the caret at the end of `D:\` +- **THEN** after Ctrl+V the field reads `D:\E:\archive` with the caret at the end + +#### Scenario: Multi-line clipboard text is flattened +- **WHEN** the clipboard holds two lines and the user presses Ctrl+V in a focused path box +- **THEN** the two lines are inserted into the path box joined by one space, and the panel cursor does not move + +#### Scenario: Shift+Ins over the panels still toggles selection +- **WHEN** the clipboard holds text and the user presses Shift+Ins with the panels focused and no path box focused +- **THEN** the cursor entry's selection is toggled and nothing is pasted + +#### Scenario: Empty clipboard is a no-op +- **WHEN** the clipboard holds no text and the user presses Ctrl+V in a focused field +- **THEN** the field and caret are unchanged + +#### Scenario: Ctrl+V over unfocused panels does nothing +- **WHEN** the clipboard holds text, no path box has focus, and the user presses Ctrl+V over the panels +- **THEN** nothing is pasted and the panel cursor does not move + +### Requirement: Terminal paste arrives as one paste + +On platforms where FileCommand's terminal input layer reports a bracketed paste as a single event — every supported platform except Windows — the system SHALL enable bracketed-paste mode while it owns the terminal, SHALL disable it before suspending for a shell command, the external editor, or the scrollback view and on every exit path including a panic, and SHALL re-enable it on resume. A reported paste received while a single-line text field owns the keyboard — the same field Ctrl+V would paste into, including a focused path box — SHALL be inserted at that field's caret as one paste operation, with line breaks replaced by a single space, exactly as "Paste into a text field" inserts clipboard text. A paste into the built-in editor's text SHALL be inserted at the caret as if typed, line breaks included; a paste into the editor's or the viewer's search or replace prompt SHALL be entered as if typed, with line breaks replaced by a single space. A paste received while no text input has focus SHALL be ignored. On Windows the system SHALL NOT enable bracketed-paste mode; a paste arrives as individual key events, which SHALL be handled as typed input, rendered as described in application-shell "Bursts of key input render once". The single-paste guarantees of this requirement, including flattening line breaks and ignoring pastes outside text input, therefore apply only off Windows. + +#### Scenario: Paste a path into the command line + +- **WHEN** a path box has focus and the terminal reports a paste of `C:\Program Files\App` +- **THEN** the path box contains `C:\Program Files\App` inserted at its caret in one operation, and the panel cursor, selection, and quick search are unaffected by the pasted characters + +#### Scenario: Multi-line paste is flattened and not executed + +- **WHEN** a path box has focus and the terminal reports a paste of two lines +- **THEN** the two lines are inserted joined by one space and nothing is selected or navigated + +#### Scenario: Paste into a dialog field + +- **WHEN** the Copy destination field is focused with the caret at the end of `D:\` and the terminal reports a paste of `backup` +- **THEN** the field reads `D:\backup` with the caret at the end + +#### Scenario: Paste with no focused text input is ignored + +- **WHEN** the delete confirmation dialog is open and the terminal reports a paste of `y` +- **THEN** nothing is deleted and the dialog stays open + +#### Scenario: Child programs get a terminal without bracketed paste + +- **WHEN** off Windows, the user runs a command from the F2 user menu, which suspends FileCommand while it runs +- **THEN** bracketed-paste mode is disabled while the command runs and re-enabled when FileCommand resumes + +#### Scenario: On Windows a paste is typed input + +- **WHEN** FileCommand runs on Windows and the user pastes `C:\Program Files\App` while a path box has focus +- **THEN** bracketed-paste mode was never enabled, the characters are inserted as typed keys in order, and the screen is redrawn once per burst of keys rather than once per character + +#### Scenario: Paste over unfocused panels is ignored + +- **WHEN** off Windows, no path box has focus and the terminal reports a paste of `C:\Program Files\App` over the panels +- **THEN** nothing changes: no box text, no cursor movement, no search diff --git a/openspec/changes/path-box/specs/theme-selection/spec.md b/openspec/changes/path-box/specs/theme-selection/spec.md new file mode 100644 index 0000000..aa0e567 --- /dev/null +++ b/openspec/changes/path-box/specs/theme-selection/spec.md @@ -0,0 +1,26 @@ +## MODIFIED Requirements + +### Requirement: Live theme preview while the picker is open + +While the theme picker is open, the system SHALL render every surface of the frame — panels, key bar, path boxes, clock, the picker dialog itself, and any overlay drawn above it — by resolving roles through the currently highlighted theme's role table, updating on the next frame after every highlight move. The preview SHALL be render-only: the active theme, the persisted configuration, and the picker's active-theme marker SHALL continue to reflect the applied theme until Enter is pressed. Closing the picker without applying SHALL restore rendering through the applied theme. + +#### Scenario: Moving the highlight previews the theme + +- **WHEN** the active theme is `nc-classic`, the picker is open, and the user moves the highlight to `purple-lights` +- **THEN** the next frame renders every element by resolving roles in the `purple-lights` table +- **AND** the active theme remains `nc-classic` and `config.toml` is not written + +#### Scenario: Esc restores the applied theme's rendering + +- **WHEN** the picker is open with `yellow-storm` highlighted and previewed, and the user presses Esc +- **THEN** the dialog closes and the next frame renders entirely through the applied theme's role table + +#### Scenario: The marker stays on the applied theme during preview + +- **WHEN** the active theme is `nc-classic` and the highlight is on `inverted` +- **THEN** the frame renders in `inverted` while the picker's active-theme marker remains on the `nc-classic` row + +#### Scenario: Opening the picker changes nothing visually + +- **WHEN** the picker opens with the highlight on the active theme +- **THEN** the rendered frame is identical to the frame before the picker opened, apart from the picker dialog itself diff --git a/openspec/changes/path-box/specs/type-ahead-jump/spec.md b/openspec/changes/path-box/specs/type-ahead-jump/spec.md new file mode 100644 index 0000000..1cdab91 --- /dev/null +++ b/openspec/changes/path-box/specs/type-ahead-jump/spec.md @@ -0,0 +1,69 @@ +## RENAMED Requirements + +- FROM: `### Requirement: Exiting type-ahead and restoring command-line routing` +- TO: `### Requirement: Exiting type-ahead` + +## MODIFIED Requirements + +### Requirement: Entering type-ahead mode with Alt+letter + +The system SHALL enter panel type-ahead quick-search mode when the user presses Alt+letter while a panel is focused, seeding the search pattern with that letter and moving the panel cursor to the first entry that matches the pattern. + +#### Scenario: Alt+letter starts a search and jumps to the first match + +- **WHEN** a panel is focused, no type-ahead is active, and the user presses `Alt+r` +- **THEN** type-ahead mode becomes active with pattern `r` +- **AND** the panel cursor moves to the first entry whose name matches the pattern `r` + +#### Scenario: Alt+letter with no matching entry + +- **WHEN** a panel is focused with no entry matching the pressed letter and the user presses `Alt+z` +- **THEN** type-ahead mode becomes active with pattern `z` +- **AND** the panel cursor does not move from its current position + +#### Scenario: Alt+letter is not routed to the command line + +- **WHEN** a panel is focused and the user presses `Alt+letter` to start type-ahead +- **THEN** the letter is consumed by type-ahead and is not inserted anywhere else + +### Requirement: Extending the pattern with printable keys + +While type-ahead mode is active, the system SHALL append plain printable keys to the search pattern and re-move the panel cursor to the first entry matching the extended pattern; plain printable keys have no other meaning over the panels. + +#### Scenario: A plain printable key extends the pattern + +- **WHEN** type-ahead is active with pattern `r` and the user presses `e` +- **THEN** the pattern becomes `re` +- **AND** the panel cursor moves to the first entry matching `re` + +#### Scenario: Extended pattern no longer matches + +- **WHEN** type-ahead is active with pattern `re` matching an entry and the user presses a printable key that makes the pattern match no entry +- **THEN** the pattern includes the new key +- **AND** the panel cursor holds its current position + +#### Scenario: Printable keys do not reach the command line while active + +- **WHEN** type-ahead is active and the user types plain printable keys +- **THEN** those keys extend the search pattern only +- **AND** none of them are inserted anywhere else + +### Requirement: Exiting type-ahead + +The system SHALL exit type-ahead mode when the user presses any panel movement key, after which plain printable keys typed over the focused panel SHALL be ignored. Esc SHALL NOT exit type-ahead; over the panels it requests application quit, and cancelling that dialog leaves type-ahead active (application-shell "Quit request keys and confirmation"). + +#### Scenario: A movement key exits type-ahead and is applied to the panel + +- **WHEN** type-ahead is active and the user presses a movement key (e.g. Down) +- **THEN** type-ahead mode ends +- **AND** the movement key is applied to the panel cursor as a normal movement + +#### Scenario: Esc leaves type-ahead active + +- **WHEN** type-ahead is active and the user presses Esc, then cancels the quit-confirmation dialog +- **THEN** type-ahead mode is still active with its pattern intact + +#### Scenario: Printable keys return to the command line after exit + +- **WHEN** type-ahead has exited via a movement key and the user then presses a plain printable key while the panel is focused +- **THEN** the key is ignored and does not start or extend a search diff --git a/openspec/changes/path-box/specs/user-menu/spec.md b/openspec/changes/path-box/specs/user-menu/spec.md new file mode 100644 index 0000000..d75badd --- /dev/null +++ b/openspec/changes/path-box/specs/user-menu/spec.md @@ -0,0 +1,15 @@ +## MODIFIED Requirements + +### Requirement: Navigate and dismiss the user menu + +The system SHALL let the user move the highlight with the Up/Down arrow keys across the `usermenu.toml` entries and the built-in `Themes` entry as one continuous list, never highlighting the separator row, and SHALL close the menu without running any command when Esc is pressed. Movement SHALL clamp at the first user entry and at the built-in entry. While the menu is open it is modal: panel and path-box input SHALL NOT be processed until it closes. + +#### Scenario: Esc closes the menu without running a command + +- **WHEN** the user menu is open and the user presses Esc +- **THEN** the menu closes, no command is dispatched to the shell, and focus returns to the active panel + +#### Scenario: Arrow keys move the highlight across both sections + +- **WHEN** the user menu lists two `usermenu.toml` entries with the first highlighted and the user presses Down twice +- **THEN** the built-in `Themes` entry is highlighted, the separator was never highlighted, and pressing Down again leaves `Themes` highlighted diff --git a/openspec/changes/path-box/tasks.md b/openspec/changes/path-box/tasks.md new file mode 100644 index 0000000..8c15cf7 --- /dev/null +++ b/openspec/changes/path-box/tasks.md @@ -0,0 +1,41 @@ +# Tasks: path-box + +## 1. Core: remove the command line + +- [ ] 1.1 Delete `State.command_line`, `history`, `history_cursor` and `State::prompt()`; delete `Command::{CommandLineClear, CommandLineHistoryPrev, CommandLineHistoryNext, PasteCursorName, PasteCursorPath, HistoryDialogOpen/…}`, the panels-level `TextEdit` / `RequestPaste` arms, and the `commands` field of `Effect::PersistHistory` (command-line REMOVED; design D7, D8). +- [ ] 1.2 Delete `recall_history`, `paste_cursor_entry`, `run_command_line`, `dispatch_cd`, `complete_cd`, `dispatch_delete_builtin`, `complete_delete_builtin`, `parse_cd` / `parse_del` / `parse_rmdir` / `parse_builtin_arg` and `ProbeIntent::{Cd, Delete}` in `update/panels.rs` and `update/command.rs`; keep `resolve_cd_target`, `parse_drive_relative`, `panels_own_input`, `start_probe` / `apply_path_probed` (reshaped in 2.5). +- [ ] 1.3 Delete `HistoryDialogState`, `handle_history_dialog`, `Overlay::HistoryDialog`, `MenuAction::History`, the Commands → History item and `config::push_history` (pulldown-menus "Menu contents"; application-shell "Modal overlays are mutually exclusive by construction"). +- [ ] 1.4 Remove `Keys.{paste_name, paste_path, clear_line}` and their parsing and serialisation; add `Keys.focus_path` (default `ctrl+g`) with parser tests for the default and the override (path-box "Focusing a path box": "Rebinding the focus key"; design D9). +- [ ] 1.5 The "Command line" help topic becomes "Path box" with new text (help-and-about "Help topic list": "Path box topic"). +- [ ] 1.6 Update or delete the affected reducer tests in `update/tests/{panels,file_op,listing,overlays,routing}.rs`, `config.rs`, `menu.rs`. + +## 2. Core: path box state and reducer + +- [ ] 2.1 `path_box.rs`: `PathBoxState { side, edit: LineEdit, completion: Option, probe: Option }`, `Completion { dir, prefix, names: Option>, candidates: Vec, index: Option, request: Option }`; pure helpers `split_last_segment`, `candidates_for(names, prefix)` (directories first, then case-insensitive name — its own comparator, not `cmp_entries`), `apply_candidate` with unit tests (design D3). +- [ ] 2.2 Commands `PathBoxFocus(side)`, `PathBoxEdit(TextEditOp)`, `PathBoxComplete { reverse }`, `PathBoxAccept`, `PathBoxCancel`, `PathBoxPaste(String)`, `CompletionsListed { request, dir, names }`; `Effect::ListCompletions { request, dir }`; `ProbeIntent::Select { parent, name }`. +- [ ] 2.3 Focus: set `path_box`, end type-ahead, make `side` active, text = cwd + separator, caret at end (path-box "Focusing a path box"; design D2). +- [ ] 2.4 Completion per design D3: listing-sourced and worker-sourced candidates, directories first, cycle and wrap, directory separator, prefix frozen for the cycle, cache kept on edits within the folder, stale reply and ignored-while-reading rules, `names: None` → `no matches` (path-box "Tab completes the last path segment", "Completion candidates and cycling order", "Completion cycle and mini-status", "Completion candidates come from a panel listing when possible", "Completion reads run off the event loop"). +- [ ] 2.5 Accept per design D4: cwd no-op, root entry, listed parent immediate (this or the other panel), probe with `Checking …`, reply guard (same side, same text, current id), success navigates/lands/closes, failure names the path and keeps focus; Cancel resets and closes and drops the outstanding probe (path-box "Enter resolves the path box text", "Enter selects the named entry", "Enter on the panel's own directory or a root", "A listed parent answers the existence check", "The existence check never blocks the event loop", "Esc cancels the path box"). +- [ ] 2.6 `ListingComplete`: exact-then-case-insensitive match for `pending_cursor_target` (design D5); find-file tests still pass. +- [ ] 2.7 Close the box whenever a layer above the panels opens (design D1): file-operation dialogs, overlays, the startup warning. +- [ ] 2.8 Reducer tests for every `path-box` scenario, run with no filesystem (application-shell "The reducer performs no filesystem I/O": all four path-box scenarios). +- [ ] 2.9 If `auto-refresh` is already merged: add `path_box.is_none()` to its probe eligibility with a test (design D10). + +## 3. TUI + +- [ ] 3.1 `input/mod.rs`: `map_path_box_key` (text-edit set, Ctrl+V / Shift+Ins, Tab, BackTab, Enter, Esc; everything else ignored) routed before `map_panel_key` whenever `state.path_box.is_some()`; `map_panel_key` drops `typing` and every command-line arm (design D7); `keys.focus_path` → `PathBoxFocus(active)`; `map_paste` targets the focused box or is ignored (text-field-editing deltas). +- [ ] 3.2 Mouse: click on a box → `PathBoxFocus(side)`; click elsewhere while focused → `PathBoxCancel` then normal handling; hit map `path_boxes: [Rect; 2]` (path-box "A focused path box owns the keyboard": "A click outside the box cancels it"). +- [ ] 3.3 `views/path_box.rs` replaces `views/command_line.rs` (design D6): two boxes on the `cmdline` row split at the panel divider, unfocused in `Role::CommandLine` with left truncation (promote `left_truncate_with_ellipsis` from `command_line.rs` / `header.rs` to one shared helper), focused via `draw_text_field` returning the caret; `views/mod.rs` call site and cursor ownership; mini-status override for `k/n matches` / `no matches` / `Reading…` in `views/panel.rs`. +- [ ] 3.4 `worker.rs`: `spawn_completion_list` using `listing::list_children_for_completion` (every readable child, no `..`); `app.rs` runs `Effect::ListCompletions`; `spawn_path_probe` unchanged. +- [ ] 3.5 `app.rs`: stop loading `history.commands` into state; `history_writer` keeps the file's `commands` array (design D8); remove `views/history_dialog.rs` and its routing. +- [ ] 3.6 Tests: `input/tests.rs` (box keys, panel keys no longer typing-dependent, Ctrl+G, rebinding, unbound keys ignored in the box), `input/mouse/tests.rs`, the `app.rs` burst test retargeted to a focused box, `snapshot_views.rs` (unfocused boxes both sides, focused box with caret, long-path truncation, matches mini-status, `Reading…`), regenerate the full-frame snapshots, `tests/integration_smoke.rs`. + +## 4. Docs + +- [ ] 4.1 README: replace the "Command line" section and its "History" subsection with "Path box"; key tables (Ctrl+G; Tab / Shift+Tab / Enter / Esc in the box; drop Ctrl+Enter, Ctrl+], Ctrl+Y, Alt+F8); `key.*` list; note the removed builtins and that Ctrl+O is unchanged. +- [ ] 4.2 F1 Help: "Path box" topic page; keyboard reference drops Alt+F8 and the paste keys; configuration topic lists `key.focus_path` and drops `key.clear_line`. + +## 5. Verify + +- [ ] 5.1 `cargo test --workspace` green; `cargo clippy --workspace --all-targets -- -D warnings` clean; `openspec validate path-box --strict` clean. +- [ ] 5.2 Manual: Ctrl+G, type `..\`, Tab cycles siblings; type a prefix, Tab, Enter → cursor lands; `D:\` Enter → enters the root; a UNC share shows `Reading…` / `Checking …` without freezing; Esc reverts; Tab over the panels still switches; Backspace still goes up; `+` still opens group select; an old config with `key.clear_line` warns at startup.