strop

configuring strop

Zero-config by default — every knob below is optional. Two files, both TOML, both layered (project over user over embedded defaults):

config.toml — the two editor knobs

At $XDG_CONFIG_HOME/strop/config.toml (usually ~/.config/strop/config.toml). Run strop config to print the live values.

tab_size = 4
indent_guides = true

languages.toml — language servers, helix-shaped

Per-project at .strop/languages.toml (next to your code, committed if you like), globally at ~/.config/strop/languages.toml. Project wins per key; unknown keys are ignored, so helix files paste clean. The workspace root anchors at the .strop directory.

Point a server at a different binary (absolute paths skip the PATH probe):

[language-server.pyright]
command = "/opt/venvs/bin/pyright-langserver"
args = ["--stdio"]

Pass server configuration — it lands in initializationOptions verbatim. The classic venv + extraPaths setup:

[language-server.pyright.config.python]
pythonPath = "/workarea/pbpilsvc/.venv/bin/python"

[language-server.pyright.config.python.analysis]
extraPaths = ["/opt/bb/lib/python3.12/site-packages"]

Reassign a language to different servers (first resolvable wins; anything unresolvable falls back to the embedded registry):

[language.python]
language-servers = ["pyright", "mypy-lsp"]

Servers stay out of your way: a missing one shows its install hint once per session, and hover/definition are gated on what the server actually advertises.

Filesystem workspace

Open a Directory buffer with strop DIRECTORY, :e DIRECTORY or :browse. Space e reveals the current source. Enter opens an entry; -/Backspace goes to its parent. Browsing does not change cwd.

:fs create nested/new.txt
:fs mkdir new-directory
:fs rename new-name.txt
:fs move destination-directory
:fs copy stored stored-copy.txt
:fs copy buffer edited-copy.txt
:fs trash
:fs remove
:apply-change
:cancel-change
:fs operations
:fs verify OPERATION STEP
:fs undo OPERATION STEP

Relative destinations use the current Directory or source parent. Review lists missing parents explicitly. Creation and rename/move never silently replace an occupied name. Stored-copy and buffer-copy select different versions without saving the original.

:fs edit opens a name-only filename draft. Ordinary motions, edits, whole-line yank/paste and undo change the draft, not disk. :w prepares review; Apply performs the checked operations. Cancel returns to the intact draft; :fs discard discards it explicitly.

:fs search opens the shared Search workspace at the captured local or SSH root. Remote search requires remote rg and is read-only: With/Review is unavailable, with no local or whole-file-download fallback.

Supported POSIX/Python SSH hosts execute real protected operations. Remote relocation retains unsaved text but revokes the old write permit; use :remote edit again before saving. Containers remain read-only. Local Trash is supported where available; remote Trash never becomes unlink. Recursive permanent removal and unsafe alias/root operations are refused.

Receipts survive leaving their view. Verify never retries a mutation, and recovery requires the recorded post-publication version, not pathname occupancy. Receipts are in-memory, not crash recovery. Unresolved outcomes block graceful global exit; explicit :qa! reports them and exits unsuccessfully.

SSH workspaces

strop +120 ssh://user@devbox/var/log/app.log
strop --tail 65536 ssh://devbox/var/log/app.log
strop --range 1048576:65536 ssh://devbox/var/log/app.log
strop --follow ssh://devbox/var/log/app.log
strop ssh://devbox/~/project/src/lib.rs
strop ssh://devbox/etc/

Use existing OpenSSH aliases, keys, agent and ProxyJump configuration. Authentication is noninteractive; host keys must already be trusted. Files open read-only by default. Normal motions, //?, visual selection, yank and splits use real buffers. Escape cancels loading or stops following; :e! refreshes. Follow stays at EOF only while you do. Changed content or shrinking windows report resets; size and mtime are not treated as file identity. Partial windows show their byte range in the modeline and use window-relative line numbers.

Press Space o or run :remote to choose a known/connected host or add one. Enter host, user@host:port or an SSH path, then press Enter to connect. New hosts start at /; successful directory choices are remembered privately. :remote home uses negotiated home expansion; :remote root returns to the root.

:tail 65536 ssh://devbox/var/log/app.log
:range 1048576 65536 ssh://devbox/var/log/app.log
:follow
:unfollow
:browse ssh://devbox/var/log/
:filter app
:remote connect ssh://devbox
:remote list
:remote disconnect ssh://devbox
:remote clear

Directory rows show file kind, POSIX permissions, server-reported byte size and native filename. Unknown attributes show ?, distinct from zero. Directory sizes are not recursive totals. Enter opens an entry; -, Backspace or ../ returns to the parent and restores the selected child. An empty :filter restores the listing. Tab completes hosts and paths without initiating authentication. Remote candidates require a live authorized connection or cached data. Documents and explicit connection pins share endpoint-owned SFTP sessions; cleanup never kills external SSH masters.

Full snapshots support remote LSP diagnostics, hover, definition/references and source/header navigation, plus Git context, staged/unstaged diffs, log, blame, commit/file navigation and source links. Services run on the remote host, never against a local lookalike path. Project-command :trust is scoped to that endpoint and workspace root. Partial/follow windows refuse full-document language services. Git mutations, save-as and arbitrary shell/filter commands remain unsupported.

Reads are bounded to 256 MiB of UTF-8 text. Tail/range edges omit incomplete UTF-8 characters. Percent-encode reserved bytes (%20, %23, %25); native Unix filenames remain byte-exact. Home expansion requires expand-path@openssh.com. Remote content is not persisted or automatically restored from sessions.

Explicit remote editing

:remote edit
:w
:remote verify

:remote edit checks a complete, non-following snapshot against the server and grants this document write authority. Edit normally and use :w or :wq. Saving runs off the input thread; a receipt for an older revision never clears newer unsaved edits. :w! cannot grant authority or bypass remote conflict checks.

The helper uses a stable cooperative lock, a private 0700 transaction directory, metadata-preserving staging, atomic replacement and file/directory syncing. It requires an owned regular single-link file, exact stored path spelling and no symlink components. Unsupported mode/ownership/mtime/attribute preservation refuses the save. Edited content is bounded to 256 MiB.

Cooperative locking is not universal compare-and-swap. Remote-save protocol participants coordinate, but another program can race the final comparison/rename window. Completed external changes are detected before commit. Private lock files retain stable identity and must not be removed while active.

Normal-mode Escape requests cancellation. If a save may have crossed rename, the result is unconfirmed and local edits remain dirty. :remote verify compares the original and intended states, syncing matching saved content before acknowledgment. It does not blindly retry, overwrite or roll back. A conflict preserves local edits; forced close can leave an unconfirmed outcome or private orphan stage.

Successful :e! refresh revokes the permit; pending/unconfirmed saves must settle or be verified first. Failed refresh preserves edits and authority. Reselecting an already-open file does not discard its draft. Remote content, permits and live writes are never restored from persisted sessions.

The remote-save contract records safety assumptions, bounded models, fault-injected real SSH checks and native-free replay evidence.

SFTP reading needs no remote Python or setup daemon. Remote editing and execution require a POSIX environment and Python 3.8+; Git/LSP also need their respective tools. Versioned interpreters on PATH are discovered; set local STROP_REMOTE_PYTHON=/opt/tools/python3.11 to select an explicit remote executable. Invalid overrides fail instead of choosing a fallback. Supervised process groups are cleaned up once the server observes lease loss. Network partitions delay detection, descendants creating new sessions can escape the group, and supervisor death cannot guarantee cleanup.

The reusable strop-remote crate owns identity, transport and execution. The workspace contract links bounded model gates, deliberate fault variants, Rust oracles and real SSH, terminal and replay evidence. This is not an unbounded proof or a replacement for OpenSSH's security boundary. See the prioritized roadmap for additional transports, directory mutations and Dev Containers.

Embedded terminals

:terminal (or :terminal COMMAND) opens a local shell in a real PTY supervised by a same-binary helper process; the directory context offers Terminal here, and :terminal-local forces a local shell from SSH/container contexts — interactive execution there is refused, never silently redirected. The VT engine is the pinned static Ghostty core: mode-aware, bounded, and identical in every build.

Input ownership: while the terminal is focused, Esc, Ctrl-C, Ctrl-R, Alt- and function-keys mean what the child expects. Ctrl-\ Ctrl-N (legacy terminals may spell it Ctrl-4 Ctrl-N) enters editor Normal mode over a pinned read-only snapshot of the grid; i/a follow the live cursor again. From terminal input, Ctrl-W is the editor's window prefix (Vim's terminal grammar): h/j/k/l/w or arrows move and cycle panes, N enters the pinned inspection, . delivers the literal byte to the child, and any other follow-up passes through unchanged. An exited session stays readable and never restarts implicitly.

Terminal buffers are read-only text buffers with the usual motions, search, selection and yank — and they are real switchable buffers: the buffer picker lists each session with Vim's ! job badge and its live phase (terminal #N · dir · running), and entering a terminal records a jump so Ctrl-O returns to your editing position. Splits mirror the live cell grid; the focused view owns the PTY geometry so two views never fight over one size. :terminal-stop stops and drains; :terminal-paste/:terminal-paste-cancel hold multiline or control-character pastes for explicit consent; :qa refuses live sessions and :qa! stops them.

Privacy and replay: terminal content is excluded from traces even with --log-content; entering a terminal records an opaque marker and downgrades the capture to metadata. --log-terminal-content is the separate explicit consent, visibly marked REC. Full --replay reproduces terminal sessions with no PTY, process, clipboard or network effects.

Source builds compile the Ghostty VT core and require an explicitly installed Zig 0.16.0; prebuilt binaries need neither. See the terminal plan for the delivered TUI milestones and the deferred GUI work.

Static syntax coverage

23 detected languages: Rust, Python, JavaScript, TypeScript, TSX, Go, C, C++, JSON, Bash, Fish, Lua, SQL, CMake, Markdown, Java, C#, Ruby, PHP, TOML, YAML, HTML and CSS. Grammars and licensed highlight queries ship in the binary; no grammar downloads or language server are required.

Markdown includes a separate inline parser, headings, emphasis, links, tables and recognized fenced languages. HTML embeds JavaScript and CSS. Syntax and structural indentation guides are revision-owned worker results; stale results cannot paint newly edited text. Long-line layout uses sparse grapheme checkpoints.

Headless scripts

strop --headless SCRIPT [+LINE] [FILE[:LINE]]

--script is equivalent. strop --help lists every directive: buffer, keys, key, paste, resize, frame, state, settle, wait and quit-intent. JSON strings use double quotes; keys takes unquoted text and tokens such as <esc> and <cr>.

keys :e source.rs<cr>
settle 5000
keys /needle<cr>
frame
state

settle [MS] drains finite owned jobs, defaults to 120,000 ms and fails on timeout. wait MS deliberately waits. Scripts retain ordered grammar typeahead; explicitly settle before relying on filesystem/service results. Headless :trust uses the same per-project/endpoint consent and private state store as the terminal UI. It never gives project configuration implicit permission to execute commands or automatically restores editor sessions.

keybindings

In the editor: :help (or Space ?) opens the full table as a real, searchable buffer — / searches it, q closes. Dispatch and the table are pinned together by tests (a key that dispatches without a row, or a live row that no-ops, fails the build) — drift is a test failure, not a doc bug. Planned-but-unshipped keys render muted with a (soon) mark. The full table also exists as docs/vim-compat.md, generated from the same source (strop --dump-compat) and pinned fresh by the gate.

sessions

Per-project sessions restore local buffers and undo history across restarts. Remote snapshots are excluded: reopening them is an explicit network action.