PCCommands¶
PCCommands is Peach Commander's command registry: the single, addressable
catalogue of every user-invokable action (cm_*), the actor that dispatches
them, and the selection state machine (SelectionState) that TC-style marking
operates on. It sits above PCVFS and depends only on PCFoundation and
PCVFS — it contains no AppKit. All UI effects are reached indirectly,
through two protocols the UI layer (PCApp) conforms to.
Scope note. This module is action identity and dispatch, not input-binding or user macros. Keyboard schemes (
Keymap, thekeymap-tc-classic.ini/keymap-macos.iniresources) and user commands (UserCommands,em_*) live inPCFoundationand are wired to this registry byPCApp— see Relationship to keymaps and user commands.
Source: Sources/PCCommands/ — PCCommands.swift, SelectionState.swift,
CommandStubs.swift.
Purpose and responsibility¶
- Define a stable, TC-compatible command vocabulary. Every action a menu
item, toolbar button, or keyboard chord can trigger is a named
cm_*command with a stable numeric id.PCAppnever calls window/panel methods by name from a keystroke path; it resolves a command name and asks the registry to execute it. - Decouple the what (a command name) from the how (AppKit windows,
panels, dialogs). Handlers talk to
PanelControllerProtocol/WindowControllerProtocol, both implemented inPCApp. - Model selection (the marked set) and cursor as an independent,
TC-faithful state machine (
SelectionState) that file operations, select-by-mask, and the status bar all read from.
Public interface and key types¶
PCCommand¶
public struct PCCommand {
public let id: Int // stable numeric id (TC id where known, else custom)
public let name: String // "cm_Copy", "cm_OpenDirUnderCursor", …
public let category: String // grouping for the command browser
public let help: String // one-line help
public let handler: CommandHandler
public let implemented: Bool // false for registered-but-unbuilt placeholders
}
A PCCommand is a value describing one action. implemented: false marks a
placeholder (see CommandStubs);
such commands appear in menus and the command browser (auto-disabled) and, when
invoked directly, report "not yet implemented".
Id ranges (a convention, not enforced by type): low ids mirror Total
Commander (cm_GoToParent = 1, cm_OpenDirUnderCursor = 2,
cm_SwitchPanel = 3, cm_SrcLong = 306…); custom commands use high blocks
(20000 volume, 30000 navigation/view/network/config, 40000 file
operations, 2000-range mark aliases); stub placeholders are auto-numbered from
50000. Uniqueness of both id and name is an invariant the registry asserts
and the tests verify.
CommandHandler and CommandContext¶
public typealias CommandHandler = @MainActor (CommandContext) async throws -> Void
Handlers are @MainActor-isolated by type. Dispatch happens on the
CommandRegistry actor (a background executor), but handlers routinely touch
AppKit, so isolating the closure type forces every handler — present and
future — onto the main actor automatically. No individual handler has to hop
threads.
public struct CommandContext {
public let activePanel: PanelControllerProtocol?
public let inactivePanel: PanelControllerProtocol?
public let windowController: WindowControllerProtocol?
public let selection: SelectionState?
}
The context is the handler's whole world. PCApp builds a fresh one at each
dispatch, wiring in the currently active/inactive panels. A handler that needs
the other panel (e.g. cm_Copy copies the active selection into the inactive
panel's directory) reads it from here.
CommandRegistry (actor)¶
public actor CommandRegistry {
public init()
public func register(_ command: PCCommand)
public func registerDefaultCommands()
public func execute(_ name: String, context: CommandContext) async throws
public func execute(id: Int, context: CommandContext) async throws
public func getCommand(_ name: String) -> PCCommand?
public func getAllCommands() -> [PCCommand]
}
The registry keeps two indices — [Int: PCCommand] by id and
[String: PCCommand] by name. register asserts (and logs) on a duplicate id
or name; a debug build traps, catching accidental collisions at development
time. registerDefaultCommands() populates the entire catalogue (all the
static let cm_* definitions plus registerStubCommands()); PCApp calls it
once at startup. execute looks the command up, then awaits
command.handler(context); an unknown name/id throws CommandError.
PanelControllerProtocol and WindowControllerProtocol¶
Two AnyObject protocols (implemented by PanelController and
MainWindowController in PCApp) that form the module's outbound
interface — the only way a handler reaches the UI.
PanelControllerProtocol— per-panel operations: navigation (goToParent,openDirUnderCursor,goBack/Forward), sorting (sort(by:ascending:)), volumes (getCurrentVolume,getVolumes,loadDirectoryFromVolume), selection actions (toggleMarkAtCursor,markAll,unmarkAll,invertSelection,restoreSelection,selectSameExtension,showSelectByMask/showUnselectByMask), file operations (copySelection(to:),moveSelection(to:),makeDirectory,deleteSelection(permanent:),packSelection(to:)), archive-aware copy (currentArchiveZipPath,copyInto(archiveZip:subPath:),reloadCurrentArchive), clipboard/edit, copy-names-to-clipboard, and tabs.WindowControllerProtocol— window/global actions:toggleActivePanel,swapPanels,showSettings, dialogs (find, multi-rename, compare, sync), view-mode toggles, network dialogs,setKeyScheme(_:),runUserCommand(_:),showNotImplemented(_:), and many more.
Keeping these as protocols in PCCommands (rather than importing PCApp) is
what lets the module compile without AppKit and lets tests exercise the registry
against nil controllers.
SelectionState (actor)¶
The TC selection model: cursor and marked set are independent.
public struct SelectableEntry: Sendable, Equatable {
public let path: String // absolute; unique key within a SelectionState
public let size: Int64 // bytes, or -1 if unknown (e.g. uncalculated dir)
public let isDirectory: Bool
}
public actor SelectionState { … }
Key behaviours (all faithful to Total Commander):
- Cursor is an index;
-1represents the..pseudo-entry, which is never aSelectableEntryand can never be marked.moveCursorUp/Down/Top/Bottom,moveCursorTo,getCursorPath,isCursorOnRoot. - Marked set is a
Set<String>of paths:select,unselect,toggleSelection(all reject..),selectAll(directories included),clearSelection,invertSelection(includingDirectories:)(TCNum*). - Selection history for undo (TC
Num/):saveSelectionToHistory/restoreSelectionFromHistory, a bounded stack (maxHistoryDepth = 50). - Select by criteria:
selectByMask/unselectByMaskmatch the leaf name against aWildcardMask(fromPCFoundation);selectSameExtensionmarks all files sharing the cursor entry's extension. - Reconciliation:
setEntriesre-seats the state after a directory reload — marks survive only for paths still present (intersection), and the cursor is clamped into[-1, count-1]. - Statistics:
getStatistics()(selected/total counts),getSelectedSize()/getTotalSize()(summing only entries whose size is known) — used by the status bar. - Operation completion:
unmarkCompleted(_:)removes only the paths a file operation reports as succeeded, so failed items stay marked.
CommandError¶
public enum CommandError: Error { case unknownCommand(String) }
CommandStubs (not-yet-implemented placeholders)¶
CommandStubs.swift registers commands that the menus and shipped keymaps
reference but whose feature is not yet built (implemented: false). Every TC
command named by a menu item or a keyboard scheme must exist in the registry so
the UI can display and route it consistently; unbuilt ones become placeholders
that auto-disable and, if invoked, call
windowController?.showNotImplemented(name). The list is a static
[(name, help)] (stubCommandList, ~26 entries such as cm_Exit,
cm_ContextMenu, cm_SrcByName) auto-numbered from id 50000. As each feature
lands, its real PCCommand replaces the stub entry.
Dependencies¶
graph TD
PCApp -->|conforms to protocols,<br/>owns registry| PCCommands
PCCommands --> PCVFS
PCCommands --> PCFoundation
PCVFS --> PCFoundation
Needs: PCFoundation (logging via PCFoundationLogger, WildcardMask) and
PCVFS (Volume, referenced by PanelControllerProtocol; PanelViewMode).
No AppKit.
Depended on by: PCApp only. MainWindowController owns a
CommandRegistry, calls registerDefaultCommands(), builds each
CommandContext, and calls execute. PanelListView imports the module for
PanelSortColumn / selection types.
Inputs and outputs¶
- Input: a command name (or id) plus a
CommandContext. Names arrive from keystrokes (viaPCApp's keymap router), menu selections, the command browser, the command line, and the toolbar. - Output: side effects performed through the two controller protocols
(opening windows, mutating panels, running file operations). The registry
itself returns nothing on success and throws
CommandError.unknownCommandon a miss.SelectionStatemutators returnBool/Int(whether/how much the selection changed) so the UI can decide when to refresh.
Lifecycle¶
sequenceDiagram
participant App as PCApp (MainWindowController)
participant Reg as CommandRegistry (actor)
participant H as Handler (@MainActor)
participant Ctrl as Panel/WindowController
App->>Reg: registerDefaultCommands() %% once, at startup
Note over App: keystroke → keymap → command name
App->>Reg: execute("cm_Copy", context)
Reg->>Reg: look up by name
Reg->>H: await handler(context) %% hops to main actor
H->>Ctrl: activePanel.copySelection(to:) / inactive.reload()
H-->>Reg: returns / throws
Reg-->>App: returns / throws CommandError
The registry is a long-lived singleton for the process. Commands are registered once; contexts are ephemeral, created per dispatch.
Threading and concurrency¶
CommandRegistryis an actor — the command map is only mutated/read under actor isolation, so registration and dispatch are race-free.CommandHandleris@MainActor: dispatch begins on the registry's executor but the handler body runs on the main thread, safe to touch AppKit.SelectionStateis an actor — cursor/marked-set/history are serialized;SelectableEntryisSendable.- This matches ADR-008 (Swift Concurrency for new code; no GCD): actors and
async/awaitthroughout, no locks.
Error handling¶
- Unknown command →
throw CommandError.unknownCommand(name), logged aterror. Callers inPCApptypicallytry?the dispatch (a bad name is a no-op, not a crash). - Duplicate registration → logged
errorplusassertionFailure(traps in debug, tolerated in release with the first registration winning — the second overwrites the map entry but the assert fires first in debug). - Handler errors propagate out of
executeto the caller; handlers arethrowsbut most current handlers are effectively non-throwing. SelectionStatehas no failure modes — out-of-range cursor moves clamp rather than throw;..marking is silently refused.
How it is tested¶
Two XCTest suites under Tests/PCCommandsTests/:
PCCommandsTests.swift— registry behaviour: registration,id/nameuniqueness across the full default set (testCommandIdsAreUnique,testCommandNamesAreUnique,testRegistryUniqueIdsAndNames), TC id mapping (cm_GoToParent==1, etc.), theimplementedflag on stubs vs. real commands (testStubCommandsMarkedNotImplemented), execute-by-name, and the unknown-command error path.SelectionStateTests.swift— ~88 tests exhaustively covering cursor moves and clamping,..handling, mark/unmark/toggle/invert,setEntriesreconciliation, select/unselect-by-mask, same-extension selection, history undo, and statistics.
Because the registry can execute against a CommandContext with nil
controllers, dispatch is testable headlessly without AppKit.
Extension points¶
- Add a command: define a
static let cm_Foo = PCCommand(id:name:category:help:handler:)and register it insideregisterDefaultCommands(). Use a custom id block (≥ 20000) unless mirroring a known TC id. The handler receives theCommandContextand drives the UI via the two protocols — never by importingPCApp. - Promote a stub: replace the entry in
CommandStubs.stubCommandListwith a realPCCommand(implemented: true) registered inregisterDefaultCommands(). - New UI capability: add a method to
PanelControllerProtocol/WindowControllerProtocol, implement it inPCApp, then call it from a handler.
Relationship to keymaps and user commands¶
PCCommands provides command identity; binding is elsewhere:
- Keymaps (
PCFoundation/Keymap.swift, resourceskeymap-tc-classic.ini/keymap-macos.ini) map a normalizedKeyChordto a command name.PCApp'srouteKeymapturns anNSEventinto a chord, asksKeymap.command(for:)for a name, and — if it is acm_*— callsCommandRegistry.execute. Layered precedence (user > active scheme > builtin) and TC id compatibility follow ADR-007. - User commands (
PCFoundation/UserCommands.swift, TC'susercmd.ini,em_*) are user-defined macros. When a chord or the command line resolves to anem_name,PCApproutes it torunUserCommandinstead of the registry.
So the dispatch fan-in in PCApp is: chord/menu/command-line → name; em_* →
UserCommands; everything else (cm_*) → CommandRegistry.
Open questions / notes¶
cm_DriveComboandcm_FreeSpaceLabelare registered asimplemented: truebut their handlers currently only log — the real drive dropdown / free-space UI is delivered byPCApp's panel chrome, so these command entries are effectively inert placeholders that escape theimplemented: falseconvention.- Several TC command names are aliased to existing behaviour (e.g.
cm_SelectAll→markAll,cm_RereadSource→reload,cm_SwitchToTargetPanel→toggleActivePanel) for keymap/menu compatibility; they are distinct registry entries, not the canonical command.