Keymap-first TUI text editor — import the keybindings you have built up in a GUI editor (such as VS Code) and edit text in the terminal with the muscle memory you already have.
Not a Vim alternative, nor a terminal port of VS Code. A plain text editor you can bring your own keymap to.
Note
Currently only VS Code keybindings.json import is supported. Other editors (JetBrains, Sublime, ...) are not supported yet.
- Imports VS Code
keybindings.json, with an import report that classifies each binding as imported / ignored / unsupported / conflict / disabled.--cmd=keep|ctrl|bothselects how Cmd-key bindings are brought in - Context-aware keybinding resolution (
Key + Context -> Action, withrescue > user > imported > defaultprecedence). While typing a key sequence, a which-key overlay shows the remaining candidates - kitty keyboard protocol support (distinguishes
Ctrl+J/Ctrl+Shift+J/Cmd+S), with a safe fallback for terminals that lack it - Command palette (
F1is a rescue entry point that always works, even with a broken config) - Editing basics: undo/redo (with grouping), find/replace, multiple buffers/tabs, line numbers, grapheme-aware Unicode handling (CJK, emoji)
- Mouse support: click to move the cursor, drag to select, wheel to scroll (SGR). In most terminals Shift+drag is left to the terminal's own selection instead of being sent to the app. In terminals that do send Shift-modified SGR events, coda ignores them, but cannot hand them back to the terminal selection
- Syntax highlighting (syntect, dark/light themes, display-only). On top of syntect's bundled languages, coda ships definitions for TypeScript/TSX, TOML, INI, Dockerfile and git files (commit message, rebase todo, config, ignore, attributes). Files without an extension (
Makefile,.bashrc,COMMIT_EDITMSG) and shebang scripts are detected by name or first line - Clipboard: OSC 52 write (copy from an SSH session to your local OS clipboard) + bracketed paste
- Input self-diagnosis:
coda inspect-key(raw input inspector) andcoda keymap verify(interactively checks whether imported bindings actually reach the app)
macOS, Linux, and Windows. Tested mainly on Ghostty and kitty-family terminals. On Windows, a ConPTY-based terminal is required (Windows Terminal recommended; modifier fidelity via win32-input-mode) — legacy conhost is rejected at startup.
Install a pinned version with the GitHub backend of mise:
mise use -g github:yano3nora/codaOr manually from Releases. Assets are named coda-v<version>-<os>-<arch>.tar.gz (.zip on Windows; a single coda binary at the archive root) with a separate .sha256 each:
curl -LO https://github.com/yano3nora/coda/releases/download/v0.1.0/coda-v0.1.0-macos-arm64.tar.gz
tar -xzf coda-v0.1.0-macos-arm64.tar.gz # put ./coda somewhere on your PATHAvailable assets:
| asset | target |
|---|---|
macos-arm64 |
macOS (Apple Silicon) |
macos-x64 |
macOS (Intel) |
linux-arm64 |
Linux aarch64 (glibc 2.17+) |
linux-x64 |
Linux x86_64 (glibc 2.17+) |
windows-x64 |
Windows x86_64 (Windows Terminal / ConPTY, .zip) |
Linux binaries are built against glibc 2.17 (zig linker). No musl static binary is provided, so glibc-based distros (Ubuntu / Debian / RHEL 7+ etc.) are the target. The Windows binary is unsigned, so SmartScreen may warn on first run.
For minimal environments without mise or even jq — an SSH server, a Docker container — a POSIX sh script handles OS/arch detection, asset download, and checksum verification (works with curl or wget; dash / busybox sh compatible):
# Install the latest version into ~/.local/bin
curl -fsSL https://raw.githubusercontent.com/yano3nora/coda/main/scripts/bootstrap.sh | sh
# Pin a version (with or without the "v" prefix)
curl -fsSL https://raw.githubusercontent.com/yano3nora/coda/main/scripts/bootstrap.sh | sh -s -- 0.1.0
# Change the install destination
curl -fsSL https://raw.githubusercontent.com/yano3nora/coda/main/scripts/bootstrap.sh | CODA_INSTALL_DIR=/usr/local/bin shAfter installing, the script runs coda --version as a smoke test and prints an export example if the destination is not on $PATH. See scripts/bootstrap.sh for details.
# Requires a Rust toolchain (rustup or mise install)
cargo install --path . # installs to ~/.cargo/bin/coda
# Or place it manually
cargo build --release # put target/release/coda somewhere on your PATHTo use it on an SSH host, just copy a release binary built for the same OS / arch:
scp coda remote:~/bin/# Open files (multiple allowed)
coda file.ts other.md
# Import VS Code keybindings (re-running overwrites)
coda keymap import vscode "~/Library/Application Support/Code/User/keybindings.json" --print-report
# For environments where Cmd never reaches the terminal: import cmd+* as ctrl+*
coda keymap import vscode <path> --cmd=ctrl # keep (default) | ctrl | both
# Press each imported binding to check it actually arrives (Esc: skip, Ctrl+C: abort)
coda keymap verify
# Inspect what your terminal actually sends
coda inspect-keyInside the editor: F1 / Ctrl+Space opens the command palette (incremental search over every action, bound keys shown alongside).
Zero configuration works by design. The default keymap follows the host OS text-editing conventions, so even when the terminal translates keys like Cmd+← into Ctrl+A, things behave as expected (Cmd+←/→ = line start/end, Opt+←/→ = word movement, Ctrl+N/P = up/down).
If you additionally want the Cmd key delivered as a real modifier (Cmd+C copy, Cmd+A select all, etc.), you need to unbind the terminal's reserved keybindings:
# Ghostty (~/.config/ghostty/config)
keybind = super+arrow_left=unbind
keybind = super+arrow_right=unbind
keybind = super+a=unbind
keybind = super+c=performable:copy_to_clipboard # pass through to coda only when there is no terminal selectioncoda tells you the exact fix instead of leaving you to guess between unbind and text:. Use coda inspect-key to see the raw bytes for any keypress; when a chord is being intercepted, both coda keymap verify and the in-editor inspector (F1 → inspector.open) print a ready-to-paste Ghostty config line plus the reason it's the right one. For example, coda keymap verify reporting that Cmd+Z arrived as Ctrl+Z prints:
MISMATCH cmd+z arrived as Ctrl+Z (edit.undo)
fix: keybind = super+z=unbind
once unbound the key falls through and is encoded via the kitty keyboard protocol. Apply, restart Ghostty (reload is not enough), then re-run `coda keymap verify`.
- Tradeoff: the shell (zsh etc.) also loses the
Cmd+←line-start translation - Fuller worked example — undo/redo,
Cmd+↑/↓, and theCmd+H-vs-Hide special case: docs/examples/ghostty.md
~/.config/coda/
config.toml # app settings (below)
bindings.json # user bindings (VS Code format + internal action names)
generated/ # import output (do not edit directly)
import-reports/ # import / verify reports
# config.toml
[appearance]
theme = "dark" # "dark" | "light"
[editor]
wrap = false # visual line wrap at startup (toggle with alt+z)
render_whitespace = false # show tab/space markers (toggle with view.toggleWhitespace)
indent_style = "space" # "space" | "tab"; what Tab / edit.indent inserts
indent_width = 4 # spaces per indent level, 1-16
[keymap]
sequence_timeout_ms = 800 # wait time for key sequences
palette_key = "ctrl+space" # convenience key for the palette (F1 always works)
ctrl_c = "copy" # set to "quit" to exit with Ctrl+C (with unsaved-changes prompt)
[terminal]
capability_warning = true # legacy terminal warning at startupIndent settings follow the buffer's .editorconfig when present (indent_style / indent_size), and can be changed per buffer from the palette (editor.toggleIndentStyle / editor.setIndentWidth). Precedence: palette override > .editorconfig > config.toml.
mise install # rust toolchain
cargo run -- <file> # run
cargo test # unit tests
mise run pre-commit # fmt --check / clippy -D warnings / test- Design docs:
docs/ADR-*.md(decisions),docs/SPEC-*.md(specs),docs/TASK-*.md(dev log) - Conventions: AGENTS.md (dependency boundaries, testing policy, scope control)
- Entry-point docs: ADR-0001 product direction / SPEC-0001 MVP scope / SPEC-0002 keybinding system
v0.1 ships as single binaries for macOS / Linux on GitHub Releases only. crates.io / Homebrew / automated publishing will be considered after manual releases stabilize the asset naming and install UX.
Releases use goreleaser (compile / archive / checksum / Release creation) plus cargo xtask (xtask/; version bump / validation / human-only publish gate). Cross builds use cargo zigbuild to produce all 5 targets (macOS / Linux × x64 / arm64, plus Windows x64 gnu) from a macOS host. mise install provides the toolchain (zig / cargo-zigbuild / goreleaser).
Manual release procedure:
# 1. Bump the version in Cargo.toml, run pre-commit (fmt / clippy / test) and toolchain
# checks, then dry-run the entire goreleaser pipeline (snapshot mode, runnable before
# tagging: generates all target assets and checksums into dist/ without pushing or
# publishing anything).
mise run release:prepare -- 0.0.0
# 2. Review the diff, commit the version bump, and tag (human).
# git diff
# git add Cargo.toml Cargo.lock
# git commit -m "Release v0.0.0"
# git tag v0.0.0
# 3. Human-only publish. Validates version / clean tree / tag=HEAD, then pushes the
# commit + tag; goreleaser rebuilds from the tagged commit and creates the GitHub
# Release (release notes generated by GitHub, token reused from `gh auth token`).
mise run release:publish -- 0.0.0 --i-understand-this-pushes-and-publishes
# 4. Smoke-test installing the published asset.
mise use -g github:yano3nora/coda@0.0.0Agents only go as far as the validation in step 1; commit / tag / push / publish are human-only (release:publish always fails without the confirmation flag). See v0.1 release readiness for implementation status and release gates.