vcom/plans/mirror-and-cursor-memory.md
vrubelroman cd877ab584 feat: mirror pane (p), cursor memory, clean up help dialog
- Add Mirror pane keybinding p (for 'Pane'), SSH restored on s
- Add session-scoped cursor memory per directory
- Fix cursor memory bug: DisplayName() adds trailing / for directories
- Fix remote mirror: use active.Path instead of mount.RemotePath
- Remove F-key/Mouse/b/c entries from F1 help dialog
- Remove Mirror from ShortHelp footer (F-keys only)
- Add missing letter-key bindings to help: p, s, a, n, x
2026-04-29 16:03:34 +03:00

3.8 KiB

Feature Plan: Mirror Pane & Per-Directory Cursor Memory

Feature 1: Mirror active pane directory to opposite pane

Keybinding research

  • Midnight Commander (MC): Alt-i — makes the other panel equal to current directory
  • Total Commander: Ctrl-PgDn opens directory under cursor in opposite panel
  • Far Manager: Ctrl-Left/Right — opens in opposite panel

Recommendation: Bind to p (for "Pane"). Ctrl-i sends the same byte as Tab (ASCII 0x09) so it conflicts with Switch. p is free, accessible, and "Pane" reflects the meaning.

How it works

  1. Read active pane's current path (including remote mount state)
  2. Apply that path to the passive (opposite) pane
  3. If active is in a remote mount, also clone the remote mount stack to the passive pane
  4. Reload both panes to reflect the change

Code changes

internal/ui/keymap.go

  • Add Mirror key binding field to KeyMap struct
  • Bind to "p" with help text "p" / "mirror pane"

internal/ui/model.go

  • Add handler handleMirrorPane():
    1. Get active pane path and remote mount state
    2. Switch passive pane to match (copy remote stack if applicable)
    3. Reload passive pane (using reloadPane or reloadRemotePane)
    4. Set status: "Mirrored: <path>"
  • Add key match case before the default switch

Feature 2: Per-directory cursor position memory (session scope)

Current state

  • enterSelected() saves pane.Path to history for back-nav, then reloads target dir with selected.Name as preserve key — but selected.Name is the name of the directory being entered, not a cursor anchor
  • goParent() pops history and reloads parent using the child directory name as preserve — cursor lands on the directory we came from
  • Missing: When navigating back into a previously-visited directory, there's no saved cursor position for it

Design

Add a cursorMemory field to BrowserPane — a map[string]string mapping directory path → last selected entry display name.

This integrates cleanly with the existing SetEntries(entries, preserveKey) / FindSelected() infrastructure.

Flow

enterSelected() / enterRemoteDir():
  1. Save to cursorMemory: pane.Path → selected entry's Name
  2. Push history (existing)
  3. Set path, reload (existing)
  4. The reload already uses preserve=selected.Name for the new dir,
     but cursorMemory will help when coming back later

reloadPane() / reloadRemotePane():
  1. Try preserve key first (existing behavior)
  2. If preserve is empty, check cursorMemory[pane.Path]
     and use that as preserveKey instead

Code changes

internal/ui/pane.go

  • Add field to BrowserPane: CursorMemory map[string]string
  • Method SaveCursor(path string, name string) — stores cursor position
  • Method LoadCursor(path string) string — retrieves saved cursor

internal/ui/model.go

  • In enterSelected() (line ~1886): after pane.PushHistory(pane.Path), add pane.SaveCursor(pane.Path, selected.Name)
  • In enterRemoteDir() (line ~5726): same save logic
  • In goParent(): save cursor before navigating away (already partially done via history)
  • In reloadPane() (line ~1672): after existing preserve logic, if no preserve key and directory has cursor memory, use it
  • In reloadRemotePane() (line ~5535): same fallback

Files modified

File Changes
internal/ui/keymap.go Add Mirror field, binding p
internal/ui/pane.go Add CursorMemory field + methods
internal/ui/model.go Add handleMirrorPane(), save/restore cursor in navigation methods

Not changed

  • internal/fs/ — storage layer unaffected
  • internal/config/ — no config changes needed
  • internal/theme/ — no theme changes needed