crystal-commander
Crystal Native Commander
Native Commander-style file manager written in Crystal, with an AppKit backend through Objective-C++ FFI.
Native macOS version of a Midnight Commander-style file manager:
- multiple panels inside one application window,
- rendering is isolated in the
ObjC++library (src/commander_renderer.mm), - panel state and navigation are controlled from Crystal (
src/commander.cr,src/renderer.cr).
Current implementation
- AppKit UI rendering and input event collection (
keyboard/mouse) are implemented in.mm. - The file model, navigation, and
Enter/Backspace/Tab/Arrowhandling are implemented in Crystal. - Built-in commands go through the Crystal
CommandRegistryandKeymap, so plugins, AppleScript, and debug automation can share the same command layer later. - Crystal exposes internal read-only snapshot structs for future plugins/debug automation.
- Crystal updates the UI through FFI commands (
set_panel_path,set_panel_rows,set_status_text,set_active_panel).
FFI architecture
src/commander_renderer.mm— native rendering library (AppKit/ObjC++).src/commander_renderer.h— C ABI for safe calls from Crystal.src/renderer.cr— Crystal wrapper over the C ABI (create/show/pump/poll/update).src/command_registry.cr— shared Crystal command layer for keyboard/menu/plugins/automation.src/keymap.cr— mapping raw key codes/modifiers to command IDs.src/snapshots.cr— read-only JSON-serializable app/panel/entry snapshots for plugins/debug automation.src/automation_protocol.cr— JSON command/response structs for future stateful IPC.src/plugin_manifest.cr— JSON manifest model for future Lua/subprocess plugins.src/plugin_host.cr— plugin manifest discovery and command registration.src/plugin_runtime.cr— plugin runtime interface, Lua subprocess runtime, and disabled subprocess runtime stub.src/sdk.cr— stable Crystal-facing facade for automation, plugins, VFS, and backend-neutral UI rendering.src/file_operations.cr— safe file operation planning structs; execution is not implemented yet.src/file_preview.cr— read-only bounded text preview forfile.view.AppSnapshot.viewer_sessions— read-only viewer session state for automation and future backend-neutral viewer widgets.src/commander.cr— example of full control logic from Crystal, without native file-control logic inside.mm.
SDK entrypoint
Crystal callers can import the SDK without launching the AppKit application:
require "commander/sdk"
See docs/SDK.md for automation, plugin, VFS, and backend-neutral UI examples.
The macOS application target uses Objective-C++ object files and framework link flags, so build the app through make commander or ./run_mac.sh rather than shards build.
Grok Delegation
scripts/grok_acp_delegate.py— minimal ACP client for delegating tasks to Grok.GROK_DELEGATION.md— workflow where Codex assigns and reviews tasks while Grok produces working patches.
Current local observation: ACP handshake works, but session/prompt may return HTTP 403: Grok Build is in early access depending on account access.
The rendering layer is isolated from behavior:
.mmdraws and emits events, while Crystal decides application behavior.
CodeSpeak specs
Architectural contracts live in specs/*Spec.cs.md:
specs/ArchitectureSpec.cs.md— Crystal-first architecture and responsibility boundaries.specs/RendererAbiSpec.cs.md— C ABI, ownership, and renderer command rules.specs/PanelsAndEventsSpec.cs.md— multi-panel model, Tab switching, cursor handling, mouse/key events.specs/PluginsSpec.cs.md— plugin API, Lua/runtime boundaries, and permissions.specs/AutomationSpec.cs.md— AppleScript/Accessibility/debug automation contracts.specs/CrystalGuiApiSpec.cs.md— future backend-neutral Crystal TUI/GUI API.specs/TabsSpec.cs.md— top-level workspace tabs in addition to multiple panels per tab.docs/SDK.md— current SDK facade, examples, and stability rules.
Before changing sensitive areas, compare the patch with the relevant spec. If code and spec diverge, the patch must either preserve the documented intent or explicitly update the spec.
C ABI API
commander_renderer_create/destroycommander_renderer_show/pump/stopcommander_renderer_poll_eventcommander_renderer_set_active_panelcommander_renderer_set_panel_pathcommander_renderer_set_panel_rowscommander_renderer_set_status_textcommander_renderer_get_mouse_positioncommander_renderer_set_mouse_visible
Running on macOS
Quick start (recommended)
cd /Users/sergey/Projects/Crystal/commander
chmod +x run_mac.sh
./run_mac.sh
Or manually:
cd /Users/sergey/Projects/Crystal/commander
make run
Manual run
cd /Users/sergey/Projects/Crystal/commander
c++ -ObjC++ -fobjc-arc -c src/commander_renderer.mm -o src/commander_renderer.o
cc -ObjC -fobjc-arc -c src/objc_bridge.c -o src/objc_bridge.o
crystal run src/commander.cr --link-flags "$(pwd)/src/objc_bridge.o $(pwd)/src/commander_renderer.o -framework Foundation -framework AppKit -framework Cocoa -lobjc -lc++"
To build the binary:
c++ -ObjC++ -fobjc-arc -c src/commander_renderer.mm -o src/commander_renderer.o
cc -ObjC -fobjc-arc -c src/objc_bridge.c -o src/objc_bridge.o
crystal build src/commander.cr -o commander --link-flags "$(pwd)/src/objc_bridge.o $(pwd)/src/commander_renderer.o -framework Foundation -framework AppKit -framework Cocoa -lobjc -lc++"
./commander
Run without timeout and without background forking. The process should stay alive until the window is closed.
Open the application and the window appears as a normal macOS window.
Configuration
PANELS— number of panels in one window (default3, range1..8).COMMANDER_PLUGIN_PATH— directory with plugin manifests (defaultplugins).COMMANDER_DUMP_STATE=1— print a read-only JSON snapshot and exit without opening a window.COMMANDER_RUN_COMMAND=<id>— execute a command ID in headless mode, print a JSON snapshot, and exit.COMMANDER_COMMAND_PANEL=<index>— panel index forCOMMANDER_RUN_COMMAND(default: active panel).COMMANDER_COMMAND_ARG=<text>— optional single string argument for headless command execution.COMMANDER_AUTOMATION_COMMAND_JSON=<json>— execute JSONAutomationCommand, return JSONAutomationResponse.COMMANDER_DRY_RUN=1— plan mutating headless commands without applying filesystem changes.COMMANDER_AUTOMATION_SOCKET=<path>— create a local Unix socket listener for stateful JSON automation while the app is running; mutating IPC commands requiredry_run=true.COMMANDER_ENABLE_LUA_PLUGINS=1— enable Lua plugin command execution through an externallua,lua5.4, orluajitbinary.COMMANDER_ENABLE_SUBPROCESS_PLUGINS=1— reserved gate for future subprocess plugin execution; runtime still stubbed.COMMANDER_VIEWER_MAX_BYTES,COMMANDER_VIEWER_TAB_WIDTH,COMMANDER_EXTERNAL_VIEWER,COMMANDER_EXTERNAL_EDITOR— configure bounded viewer/editor metadata exposed in snapshots.
Read-only state through the wrapper:
sh scripts/commanderctl state
sh scripts/commanderctl commands
sh scripts/commanderctl status
sh scripts/commanderctl plugin-list
sh scripts/commanderctl runtime-list
sh scripts/commanderctl command file.view 0
sh scripts/commanderctl command-json '{"command_id":"panel.open_path","panel_index":0,"argument":"/tmp","dry_run":false}'
sh scripts/commanderctl command-json-file /tmp/commander-command.json
sh scripts/commanderctl ipc-command-json /tmp/commander.sock '{"command_id":"app.help"}'
sh scripts/commanderctl ipc-command-json-file /tmp/commander.sock /tmp/commander-command.json
sh scripts/commanderctl ipc-state /tmp/commander.sock
sh scripts/commanderctl ipc-status /tmp/commander.sock
COMMANDER_COMMAND_ARG=/tmp sh scripts/commanderctl command panel.open_path 0
sh scripts/commanderctl open /tmp 0
sh scripts/commanderctl view README.md
sh scripts/commanderctl mkdir /tmp/commander-demo-dir 0
sh scripts/commanderctl mkdir /tmp/commander-demo-dir 0 --dry-run
sh scripts/commanderctl copy /tmp/commander-demo-dir 0
sh scripts/commanderctl copy /tmp/commander-demo-dir 0 --dry-run
sh scripts/commanderctl move /tmp/commander-demo-dir 0 --dry-run
sh scripts/commanderctl delete 0 --dry-run
commanderctl uses the linked ./commander binary and builds it through make commander if needed, so native AppKit/ObjC++ link flags stay centralized in the Makefile. Command snapshots include plugin_id for manifest-declared plugin placeholders and mutating metadata for automation policy decisions.
CodeSpeak spec shape check:
sh scripts/spec_check
Validation checklist:
cat scripts/smoke_plan.md
Read-only Grok ACP review helper:
sh scripts/grok_review .grok-acp/some_task.md
Bounded Grok ACP worker helper:
sh scripts/grok_worker .grok-acp/some_patch_task.md
Plugin manifests
Plugin discovery is metadata-only for now. Example:
{
"id": "example.hello",
"name": "Example Hello Plugin",
"version": "0.1.0",
"api_version": "0.1",
"runtime": "lua",
"entrypoint": "main.lua",
"permissions": [],
"commands": [
{"id": "example.hello.status", "title": "Hello Status"}
]
}
Manifest-declared commands are registered through the command layer and are executed by their configured runtime when that runtime is enabled.
Runtime request/response structs are JSON-serializable so embedded Lua and future subprocess runtimes can share a protocol:
{
"command_id": "example.hello.status",
"plugin_id": "example.hello",
"entrypoint_path": "/absolute/path/to/plugins/example/main.lua",
"context": {"active_panel": 0}
}
Lua plugin MVP
Lua plugin execution is disabled by default and must be enabled explicitly:
COMMANDER_ENABLE_LUA_PLUGINS=1 sh scripts/commanderctl command example.hello.status 0
COMMANDER_ENABLE_LUA_PLUGINS=1 ./commander
The runtime looks for COMMANDER_LUA_BIN, then lua, lua5.4, and luajit on PATH.
Available Lua API:
commander.command("example.hello.status", function(ctx)
commander.status("Hello from Lua plugin metadata example")
end)
Available MVP context fields:
ctx.command_idctx.plugin_idctx.active_panelctx.panel.pathctx.panel.urictx.panel.display_pathctx.panel.cursorctx.panel.entriesctx.panel.selected_entryctx.panel.marked_paths
Available VFS intent helpers:
local parsed, err = commander.vfs.parse(ctx.panel.uri)
local schemes = commander.vfs.allowed_schemes()
local action, action_err = commander.vfs.request("stat", ctx.panel.uri)
Lua VFS helpers are permission-gated by manifest entries such as vfs.read:file. commander.vfs.request returns a Commander-mediated intent action; it does not execute provider I/O inside Lua.
Current safety boundaries:
- Lua receives copied snapshot data, not Crystal heap pointers.
- Lua can update status text through
commander.status. - Lua can declare read-only VFS intent actions for Crystal to mediate.
- Lua cannot directly mutate panels, call AppKit, call renderer C ABI, run shell commands through Commander, or perform provider I/O by itself.
crystal-commander
- 2
- 0
- 0
- 0
- 0
- about 1 month ago
- April 22, 2026
Thu, 23 Apr 2026 04:11:14 GMT