docs/CONFIGURATION.md查看源文档 ↗
本页目录

Configuration

Documentation index · Project overview

Every setting Corta has, in the one file that holds them: ~/.config/corta/config.

That file is the single source of truth for settings — what this document covers. The Settings page (⌘,) edits it and reads the result back; there is no second settings store. An edit made in $EDITOR while Corta is running is picked up within a moment — the file and its directory are both watched, because most editors write a temporary file and rename it over the target rather than writing in place. App-owned history, window arrangements and remote-edit copies are separate from settings (§8).

The file is created with the current values the first time the Settings page is opened, so opening it once and then reading the file is the fastest way to see the current defaults. For the separate development app, the file is ~/Library/Application Support/Corta Dev/config; an explicit CORTA_STAGE_DIR uses <stage>/config. See Testing.


1. Format

key = value        # comment to end of line
  • One setting per line. Whitespace around the key, the = and the value is ignored.
  • A line whose first non-blank character is # is a comment.
  • A # that opens a value, or follows a comma in a list, is a colour, not a comment (background = #101018, ansi = #000, #c23621). The key is split off before comments are stripped, which is what makes both readings possible.
  • Booleans accept true/false, yes/no, on/off, 1/0.
  • An unknown key is preserved, not dropped: a file written by a newer Corta survives a round trip through an older one.
  • An unparseable line is skipped, never fatal. A typo in one setting must not cost you every other setting, and the terminal has to start.
  • A file that exists but is not UTF-8 text, or cannot be read, is never written: Corta keeps its current settings and Settings says why changes are not being saved, until the file reads again.
  • Numeric values outside the documented range are clamped and written back in their clamped form. Invalid numbers, unsupported enum values and malformed shortcuts keep the default for that key when loading the file; their original lines are preserved on a settings round trip.

Three families of key are structured rather than scalar, and all use a dotted prefix so the flat format needs no nesting: theme.<name>.… (§4), preset.<name>.… (§4a) and bind.<command> (§5).


2. Settings

Appearance

KeyValuesDefaultNotes
themea theme name, or light:<name>,dark:<name>cortaOne name serves both appearances; the pair gives light and dark mode a theme each, and a side left out is corta; a name with : or , in it cannot be chosen. Built in: corta, solarized and mono, all offered in the UI (§4). A theme defined in this file wins over a built-in of the same name.
appearanceauto, light, darkautoWhich of the theme's two variants is live. auto follows macOS and switches while running.
status-barbooleanfalseShow a bottom bar of local system metrics. Click it for local host details. Sampling pauses when every enabled bar is hidden.
status-itemscomma-separated cpu, load, memory, network, disk, thermalall sixChoose metrics to collect and display. Empty shows the local-host details button without periodic sampling.
status-network-interfaceauto or an interface nameautoAuto selects one primary interface (physical fallback); names such as en0 or utun2 select exactly that interface. An unavailable explicit interface shows no rate. VPN and physical traffic are never summed.
cursor-shapeblock, bar, underlineblockDefault cursor shape, applied live. Terminal programs can temporarily override it with DECSCUSR; parameter 0 or a terminal reset restores this default.
cursor-blinkbooleanfalseBlink the default cursor every half second while its pane has keyboard focus and is visible at the live screen. Terminal programs can temporarily override blinking with DECSCUSR.
font-familysystem, or an installed family namesystemsystem is the macOS system monospaced font. Any other value is a family name (Menlo, JetBrains Mono) and is kept as written; it is used only if it is installed and its regular, bold and italic faces share one advance across printable ASCII (D12), otherwise System Monospaced draws instead and Settings ▸ Appearance shows why. Settings lists only the families that pass. Characters the family lacks come from font-family-cjk and the faces font-fallback lists.
font-family-cjkauto, or an installed family nameautoThe family Chinese, Japanese and Korean characters the font lacks are drawn in, first in the fallback. auto follows the system language (see font-fallback). Any other value is kept as written and used if it is installed and has Han, kana or Hangul in outline; it need not be monospaced, since it draws only CJK, centred and fitted to its cells, and the font keeps the cell size and baseline. What it lacks falls through to the auto faces. Otherwise auto applies and Settings ▸ Appearance says so. Unused while font-fallback is false.
font-size10–24, whole points13Points; a fractional value rounds to the nearest whole point, applied immediately to panes without a temporary zoom and used by new windows and panes. Font changes never move or resize the window; they refit the terminal rows and columns to it, and what is left of a cell shows as background under the last row and past the last column. Very small windows remain subject to minimum-size constraints. ⌘+ / ⌘− / pinch (B09) are a temporary, per-window zoom that never writes here — zooming one window does not change this key, another open window, or what the next new window opens at; ⌘0 ends the zoom and returns to whatever this key currently says. New panes in the same window inherit its temporary zoom. A zoom does not survive quitting and relaunching Corta.
font-weightlight, regular, mediumregularThe primary face's weight. A family without that face, or whose face fails the same width check as the family, draws its Regular (Menlo and PT Mono have only Regular and Bold; SF Mono and Fira Code have all three). Bold text is always the family's own bold.
line-spacing1.0–1.4, in steps of 0.021.18The line's height as a multiple of the font size. A font that declares a taller line keeps it, so 1.0 never clips; CJK families that declare exactly one em get room between rows. Every cell in every language shares it.
font-fallbackbooleantrueCharacters the font lacks come from other fonts: PingFang SC or TC, Hiragino Sans and Apple SD Gothic Neo by the user's language (kana always Hiragino Sans, Hangul always Apple SD Gothic Neo), then Apple Color Emoji, then whatever macOS finds. false draws them as the missing-glyph box.
emoji-renderingcolor, monochromecolormonochrome draws an emoji as a text glyph where the primary font, Apple Symbols or Menlo has one; an emoji none of them has stays in colour. Width follows the terminal's width rules either way.

Window

KeyValuesDefaultNotes
columns20–500120The grid a new window opens with, in cells.
rows5–30030As above. The window's size in pixels is this grid times the font's cell metrics plus the pane insets — so two terminals showing the same columns × rows are still different sizes on screen if their fonts differ.
restore-windowsbooleantrueReopen window frames, tabs, splits, divider proportions and working directories on the next launch. Starts fresh child processes; terminal text and running jobs are not restored.
confirm-closebooleantrueAsk before closing a pane, window or the app while a shell still has a foreground job.

Terminal

KeyValuesDefaultNotes
scrollback-lines0–100000010000Lines of history per session. Applies to sessions opened afterwards: a running child's history cannot be re-limited without discarding lines.
ambiguous-widthnarrow, widenarrowEast Asian Ambiguous characters (Unicode UAX #11, version 17.0) — ±, §, é, Greek, Cyrillic, box drawing among them — take one cell or two; wide box drawing spans both cells. Applies to output written after the change; what is already on screen keeps its width. Programs that measure text themselves (shells, editors) count by their own rules, so wide can misalign them.
command-history-limit0–10000512How many completed/running commands a session's structured command history (jumping, copy/export by identity, Command History search) keeps at once — separate from scrollback-lines, which bounds visible text, not command records. Applies to sessions opened afterwards, same reason as scrollback-lines.
bellvisual, audible, mutedvisualvisual flashes the pane; audible is NSSound.beep().
input-source-indicatorauto, always, offautoA persistent input-source badge in the window toolbar by default. auto enables it only when the user has enabled a non-Latin keyboard layout or an IME; always shows it for any input source. Every mode hides it in unfocused panes. In the toolbar it stays while a program runs, since it covers no output. With prompt placement, auto shows it at a ready shell prompt (without OSC 133 shell integration it falls back to the focused pane), always also during command output, and both hide it in scrollback and alternate-screen programs. With prompt placement, long input moves it downward without changing the grid; if no safe space remains it hides until the next prompt. Toolbar placement stays fixed. Applies immediately. Settings ▸ Keyboard & Mouse.
input-source-indicator-positiontoolbar, prompttoolbarFixed window-toolbar status separated from network/file actions, or the original right-edge prompt overlay with long-command avoidance. Uses the focused pane’s state. Off and prompt placement remove the toolbar slot and its spacer. Applies immediately. Settings ▸ Keyboard & Mouse.
input-source-direct-colorhex color, or empty(empty)Background for confirmed direct input (A). Empty uses quiet text over a faint fill: the theme's text colour when the badge sits beside the prompt, the system's in the toolbar. Accepts #rgb / #rrggbb; text automatically uses black or white for contrast.
input-source-ime-colorhex color, or empty(empty)Background for a non-Latin keyboard layout or a reported built-in IME mode (中, あ, 한). Empty uses a soft indigo tint beside the prompt and gray in the toolbar. Third-party IMEs that do not expose their internal ASCII mode keep a neutral badge, independent of this color.
option-as-metabooleanfalseWhether ⌥ acts as Meta — an ESC prefix on the base character, the way a PC keyboard's Alt does — instead of composing the layout's alternate character. Off by default because on macOS ⌥ is text input: it types é, ø, –, and starts dead-key sequences, and an international layout needs that. Turn it on when a program wants M-x and M-b. Special keys are unaffected either way: ⌥ already reaches the child there as the xterm modifier parameter, and an IME still sees every event it would otherwise see.
open-file-commandstring(empty)The command run when a path:line reference in program output is ⌘-clicked. {file}, {line} and {column} are substituted, one argument at a time. The executable must be an absolute path and is run directly — never through a shell — so a path containing ; or $(…) stays a path. For both local and remote references, a nonempty editor command is required. Empty disables opening: system default handlers can execute some file types. The hover tooltip names the required setting. The same template is used for the managed local copy the remote-editing flow downloads — {file} is the copy, the line and column are the reference's own.
search-regexbooleanfalseWhether the search field is read as a regular expression (ICU syntax, as NSRegularExpression accepts it). The * button in the search bar writes this key. Three things are reported rather than shown as "no results": a pattern that does not compile, a pattern whose shape makes a backtracking engine take exponential time ((a+)+, (a*)*, (a|a)+ — every one has a linear equivalent, and it is refused before it runs because ICU's time limit is not reachable from Swift), and a sweep that stopped on its 500 ms budget or on a line longer than 64,000 units, which the match count marks with a +.
search-case-sensitivebooleanfalseWhether scrollback search distinguishes case. Off by default: a person searching a log for error wants Error and ERROR too. The Match Case button in the search bar writes this key, so the choice survives closing the bar and restarting.
paste-warningdangerous, always, offdangerousWarns for shell pipes (and bash <(curl …) / sh -c "$(curl …)" feeds), sudo, recursive forced deletion, backslash continuations, hidden Unicode characters, and line breaks without bracketed paste. always also warns for any multiline paste with bracketed paste enabled; off disables dialogs. Applies immediately. “Don’t warn again for this pane” lasts until the pane closes.
copy-on-selectbooleantrueA finished selection goes straight to the clipboard, confirmed by a label in the corner of the pane. Set false for ⌘C only.
mouse-override-modifieroption, shift, controloptionHold this modifier when beginning a drag to select terminal text while a TUI owns the mouse. Applies immediately. The pane shows a hint on first use; copy-on-select controls automatic copying.
link-activationcommand, clickcommandcommand opens a link on ⌘-click. click opens it on a plain click and underlines the link under the pointer; dragging across a URL still selects it.
allow-clipboard-writebooleanfalseWhether OSC 52 may put text on the system clipboard — the only route from inside tmux or an ssh session. Off by default because any output could use it. The read direction does not exist under any setting (SECURITY.md §6).
secure-keyboard-entrybooleanfalseSecure Keyboard Entry: while a Corta terminal window is key and Corta is the active application, the system stops delivering keystrokes to any other process — event taps, keyloggers, macro tools and accessibility clients alike. Shell ▸ Secure Keyboard Entry toggles this key and shows a checkmark; a lock in the titlebar shows when it is actually engaged, which is not while another app is frontmost or Settings is key. Off by default because it is system-wide and also silences the tools a person may rely on (SECURITY.md §4.5).

Following a file reference

⌘-click on src/main.rs:42:17 in program output opens that file, at that line where open-file-command can express one. A URL wins where both could match.

Start with the editor's installed command-line executable. Run command -v code (or your editor's CLI name) in a shell and use its absolute path:

open-file-command = /usr/local/bin/code --goto {file}:{line}:{column}

Replace /usr/local/bin/code with the actual executable on your Mac. The template is split on whitespace before placeholders are substituted; quoted paths, ~, $HOME, pipes and other shell syntax are not interpreted. For an executable in a path containing spaces, use a command-line launcher or symlink whose path contains no whitespace. Referenced file paths containing spaces remain one argument after substitution. Empty disables both local and remote file opening.

Local and remote references share this editor template, but resolve in different places:

  • A bare path is not a reference. Only path:line — the shape a tool emits — is detected. Ordinary output is full of things that look like paths (and/or, n/a, TODO/FIXME), and underlining a third of every line teaches you to ignore underlines.
  • Local references: resolve against the pane's confirmed local working directory and must name an existing regular file. A remote OSC 7 report never becomes a local working directory.
  • Remote references: use the pane's known remote host and directory. After host confirmation and SFTP authentication, Corta downloads a managed local copy and opens it with the configured editor. It never treats the remote path as a path on your Mac. The copy is limited to 64 MiB; uploading edits requires an explicit decision and a remote-content conflict check. See Remote work.

This does not widen the URL scheme allowlist (SECURITY.md §2.4). A file:// string in output is still plain text and still cannot be detected as a link. Output-derived files are opened only through your configured editor, never through a system default handler. Paths are substituted as arguments and the command runs directly, without a shell.

Shell integration

Two paths provide the same bundled hooks. Standard local zsh sessions load them automatically when directory-completion = true or history-suggestions = true (both default to true), without editing startup files. Custom zsh arguments, bash, fish and remote shells need explicit installation or their own compatible OSC 133 hooks. Disabling suggestions in a live shell does not unload hooks already sourced; new sessions with both settings off do not use the automatic zsh bootstrap. Directory and history suggestions come only from that automatic bootstrap: installed hooks give a shell the OSC 133 marks, not suggestions.

Startup-file installation is not a config-file key — it edits the login shell's startup files, which only Settings ▸ Terminal ▸ Shell Integration touches, and only inside one marked block each. The shell is the one $SHELL names: ~/.zshrc for zsh, ~/.config/fish/config.fish for fish, and zsh's for anything else. bash takes two: Corta starts it as a login shell, which reads the first of ~/.bash_profile, ~/.bash_login and ~/.profile that exists (a new ~/.bash_profile when there is none) and never ~/.bashrc, while a bash started inside the session reads only ~/.bashrc — so the block goes into both, and guards itself against running twice when one sources the other. It only acts in an interactive bash, so bash -lc output and other shells reading ~/.profile are untouched.

# >>> Corta shell integration >>>
…
# <<< Corta shell integration <<<

Install appends the block (creating the file first if it does not exist); Remove deletes exactly that block and nothing else a user wrote around or inside it — for bash, from every login file it may have gone into — and deletes a file only if Install created it (its first line says so) and nothing else is left in it. Installing is reversible for the same reason: nothing outside those two lines is Corta's to change, so removing them undoes the whole thing. Installing twice changes nothing the second time, and the script itself guards its own hooks with CORTA_SHELL_INTEGRATION_ACTIVE in case something else sources it again.

The row reports one of four states, read fresh from those files every time Settings opens — the file, not a cached flag, is the ground truth, the same rule the config file itself follows (§1):

StateMeaning
Not installedNo Corta block. Offers Install.
InstalledThe block is present and holds this version's hooks. Offers Remove.
Installed hooks differThe block is present, but what sits between its marker lines is not this version's hooks — an earlier version's, or edited by hand — or, for bash, it is in one of its two files and not the other. Corta never rewrites the block on its own, so a fix to the hooks waits here. Offers Update, which replaces everything between the two marker lines, edits included, and leaves the block where it is; and Remove. A block whose end marker is missing reads as Installed, since there is nothing whole to update.
Possible conflictNo Corta block, but the file already sources another terminal's own integration (iTerm2, Starship, VS Code or WezTerm's are recognised by name). Offers Install Anyway — installing alongside another integration is not refused, only flagged, since only the user knows whether that is what they want.

The installed script emits OSC 133 ; A/B/C/D and OSC 7 for the working directory from the shell's own hooks — zsh's preexec/precmd, bash's DEBUG trap and PROMPT_COMMAND, and for fish a wrapped fish_prompt plus fish_preexec (fish 4 sends most marks itself, and the script then adds only the one it lacks) — the same two sequences Performer+ShellIntegration.swift and this document's command-jump entries already describe. Startup-file blocks are installed only on request. Automatically bootstrapped local zsh sessions have the same command boundaries without such a block. A session with neither kind of integration uses TaskNotifier's typing-and-idle heuristic.

Notifications

KeyValuesDefaultNotes
notify-from-programsbooleantrueAllow bounded program notifications and agent attention notices. Notification Center may retain their message text.
agent-statusbooleantrueShow state on tabs, the Dock and the Agents list. Dedicated task-state integration currently covers Claude Code only; process-only detections show State unknown.
agent-processescomma-separated names(empty)Additional foreground executable names, up to 32 names of 80 bytes each; macOS reports at most the first 32 bytes of a process name, so a longer name never matches. Built-ins: claude, claude-code, codex, gemini, aider, opencode. Names enable process detection only, not task-state integration or hook installation. No polling timer.
notify-on-long-taskbooleanfalsePost a notification when a long command finishes.
notification-threshold≥ 130Seconds a command must run to be worth one.

Program notifications use notify-from-programs and the visibility/occasion rules described in the user guide. Claude's preferredNotifChannel and terminalProgressBarEnabled are Claude settings, not Corta config keys. auto does not choose a message channel for Corta. Select ghostty, iterm2 or kitty in Claude for message text; notifications_disabled there does not disable Corta's fixed 7501 attention. Corta keeps its own terminal identity and does not spoof another terminal to enable Claude's progress channel. These notices are independent of the long-task threshold below. Process-name detection does not provide dedicated task-state integration for another CLI.

A long-task notification (notify-on-long-task) is posted only when all of these hold: the setting is on, the command ran longer than the threshold, the window is not the key window, and macOS has granted notification permission. With shell integration (OSC 133) the boundaries are exact; without it, Return starts the timer and 1.5 s of output silence ends it — which is why the feature is off by default. The notification carries the tab's name you gave it, or else the pane's directory, and the exit status — never the command text or a title a program set (SECURITY.md §5).

History

KeyValuesDefaultNotes
history-suggestionsbooleantrueFaint suffix from the most recent matching command in local zsh’s in-memory history. Right or End accepts at the end of a nonempty, single-line input; Esc dismisses. Tab, Shift+Tab and Up/Down keep native bindings. cd retains directory-suggestion precedence while directory-completion is on. Disabled automatically when zsh-autosuggestions is loaded. No startup-file edits or history-file reads. Restart the shell after enabling. See History suggestions.
directory-completionbooleantrueFaint folder alternatives below cd, with an inline suffix as you narrow the matches in a standard local zsh session. Tab fills the selected path; Left/Right select the previous/next candidate; Esc dismisses. Up/Down retain shell history navigation and Shift+Tab goes to the shell. Set to false to disable suggestions and restore native key handling. New sessions load the bundled hooks without editing the user's startup files. Restart the shell after enabling. Bash and fish retain native completion. See Directory suggestions.
command-status-marksbooleantrueShow command outcomes in the left margin: green for exit 0, red for other errors, and a split gray line for interruption (exit 130). Hover for the textual outcome.
directory-historybooleantrueWhether Corta records the local directories a completed command actually ran in (OSC 7). Nothing in the app lists the record yet; Settings ▸ Privacy & Security ▸ History shows its size and clears it.

Not the config file's own state: directory-history only gates whether DirectoryHistoryStore reads and writes its file (Application Support, not ~/.config/corta/config — the same split SessionRestore draws between settings and app-managed state). Turning the key off stops it being read or written; Clear in Settings ▸ Privacy & Security ▸ History empties it (and deletes the file) regardless of the setting. Entries are ranked by frecency — a visit count that halves every three days — and at most 1,000 are kept; the ranking has no reader in the app yet. DirectoryHistory.projectRoot(for:) separately finds the nearest ancestor directory containing .git, for jumping to a project root rather than wherever inside it a command happened to run. A directory only ever enters this history already local — OSC 7's own host check (Performer+OSC.swift) drops a remote report before TerminalSession .currentDirectory ever reports it, so nothing here can rank a path that belongs to a different machine.

An app-initiated directory change (PaneCommands.changeDirectory(to:)) writes cd '<path>' to the child only when ViewController.canChangeDirectorySafely holds: shell integration is active, no command is currently running, and the cursor is still exactly where the prompt finished drawing — so a directory a person picked from history never lands on a busy shell, a TUI holding the screen, or a prompt with something already typed into it.

Updates

KeyValuesDefaultNotes
update-auto-checkbooleantrueWhether Corta checks appcast.xml in the background, once a day.
suggest-applications-folderbooleantrueWhether Corta offers to move itself into /Applications on launch, when it is not already there.

Check for Updates… (the Corta menu, directly under About) always works, on or off — this key only gates the unattended check nobody asked for. Updates are fetched over HTTPS and verified against an EdDSA signature before installing (UpdateController.swift, docs/DESIGN.md — Sparkle is the one third-party dependency in the app shell; the terminal core has none).

suggest-applications-folder turns itself off the first time the prompt is answered either way — accepting moves the app and there is nothing left to ask about, and "Don't Ask Again" writes false directly (ApplicationsFolderMover.swift). New direct downloads use a disk image with an Applications link (D26). The prompt still helps when an app is launched from the image or another folder without being copied first; Sparkle and Spotlight/Launchpad expect /Applications.


3. A complete example

Every scalar key, at its default unless the comment says otherwise. The structured families — themes (§4), presets (§4a) and shortcuts (§5) — are shown in their own sections.

# Appearance
theme = corta
appearance = auto
font-family = system
font-family-cjk = auto
font-size = 14                     # default 13
font-weight = regular
line-spacing = 1.18
font-fallback = true
emoji-rendering = color
cursor-shape = block
cursor-blink = false
status-bar = false
status-items = cpu,load,memory,network,disk,thermal
status-network-interface = auto

# Window
columns = 120
rows = 30
restore-windows = true
confirm-close = true


# Terminal
scrollback-lines = 50000           # default 10000
ambiguous-width = narrow
command-history-limit = 512
bell = visual
input-source-indicator = auto
input-source-indicator-position = toolbar
input-source-direct-color =
input-source-ime-color =
option-as-meta = false
open-file-command =
search-regex = false
search-case-sensitive = false
paste-warning = dangerous
copy-on-select = true
mouse-override-modifier = option
link-activation = command
allow-clipboard-write = false
secure-keyboard-entry = false

# Notifications
notify-from-programs = true
agent-status = true
agent-processes =
notify-on-long-task = true         # default false
notification-threshold = 60        # default 30

# History
directory-history = true
directory-completion = true
history-suggestions = true
command-status-marks = true

# Updates
update-auto-check = true
suggest-applications-folder = true

4. Themes

A theme is the sixteen ANSI colours plus the three the terminal owns (default foreground, default background, cursor), in a light and a dark variant. The 6×6×6 colour cube and the 24-step greyscale ramp are not part of a theme: xterm defines them numerically, so a program asking for colour 137 means one specific colour.

Corta offers three previewable themes: corta, solarized and mono. Settings → Themes lists them once for light mode and once for dark mode, and each mode keeps its own choice (theme = light:solarized,dark:corta). Existing custom selections and inheritance continue to work.

Keys

KeyMeaning
theme.<name>.nameThe display name in the Settings page and the View menu. Defaults to <name>.
theme.<name>.inheritA built-in to start from: corta, solarized, mono. Defaults to corta.
theme.<name>.<variant>.foregroundDefault text colour. <variant> is dark or light.
theme.<name>.<variant>.backgroundDefault background.
theme.<name>.<variant>.cursorCursor colour, for every cursor style. A block cursor is opaque, and the character under it is drawn in the background colour, as Terminal.app draws it.
theme.<name>.<variant>.ansiThe whole table on one line, comma-separated. A shorter list overrides a prefix of it.
theme.<name>.<variant>.ansi<N>One slot, N from 0 to 15: black, red, green, yellow, blue, magenta, cyan, white, then the eight bright ones.

Colours are #rgb or #rrggbb; the # is optional. There is no alpha component — the terminal surface is opaque content, not a glass layer.

Anything left unset is inherited, so a two-line theme is a legal theme — and a half-written one still renders, which matters because this file is hand-edited.

Derived colours

The highlights drawn over the text have no keys: they are worked out from the variant's own colours, so a light theme gets a selection that shows on a light background and a custom theme needs nothing extra. Fills sit under the characters, which keep their own colour.

  • Selection: blue (ansi4) moved 40% of the way towards the foreground, at 40% opacity.
  • Search matches: yellow (ansi3) at 35% opacity; the current match at 55%.
  • Hovered link: an underline in cyan (ansi6).
  • Command marks: green (ansi2) at 85% opacity for a command that succeeded, red (ansi1) at 90% for one that failed, and the midpoint of the foreground and background at 90% for one that was interrupted.

Only the first eight slots are used: in some themes, Solarized among them, the bright slots are grey tones rather than brighter hues.

theme = midnight

theme.midnight.name = Midnight
theme.midnight.inherit = solarized
theme.midnight.dark.background = #101018
theme.midnight.dark.cursor = #00c2c7
theme.midnight.dark.ansi1 = #ff5f56

Graphical theme editing

View → Theme editor opens the editor directly. Settings → Appearance → Create theme copies the selected theme into an unsaved draft. Edit dark and light variants using color pickers or HEX input; the preview is local to the editor. Save theme writes a new custom theme and selects it. Edit theme is offered for custom themes; built-ins are copied rather than modified. Cancel discards the draft. Restore source colors restores the current variant to the colors it had when the editor opened. Invalid colors disable saving. An external edit to the same custom theme requires reopening the editor before saving.


4a. Presets

A preset is a named way to open a terminal: a shell, a directory and a few environment variables. Shell ▸ New Pane with Preset lists them, in the order the file defines them, and hold ⌥ to open one in a window of its own. With no presets defined the row is hidden rather than greyed out: a submenu's parent item carries no action, so AppKit enables it unconditionally and a disabled-looking row would still open an empty menu.

preset.api.shell = /bin/bash
preset.api.arguments = -l
preset.api.directory = /Users/you/src/api
preset.api.env.API_ENV = staging
preset.api.env.NO_COLOR = 1

Holding ⌥ while choosing a preset opens it in a window of its own instead of splitting the focused pane.

Every field is optional. shell and directory must be absolute — a relative path would resolve against whatever Corta was launched from, which on a Finder launch is / — and a preset that names neither an absolute path nor any setting at all is ignored rather than offered.

env.* variables are added on top of the sanitised inherited environment (SECURITY.md §4.3): a preset can add and override, never remove. A preset whose shell has been uninstalled degrades down the same fallback ladder an ordinary pane uses, so it opens a working terminal rather than a failure panel — and the preset's arguments apply only to its own shell, since a fallback /bin/sh may not understand them.

A preset is also the supported way to open a remote terminal:

preset.staging.shell = /usr/bin/ssh
preset.staging.arguments = staging.example.com

The child is the system's own OpenSSH client, so everything its configuration does — Host blocks, Include, Match, ProxyJump, agent keys, ControlMaster connection sharing — applies exactly as it does in any other terminal; Corta parses none of it. directory, if set, remains a local working directory for the launcher. The pane's title carries a Remote: badge with the remote host and directory once the remote shell reports them (OSC 7), and says so when it cannot know them. When the connection dies, Shell ▸ Terminal and Connection ▸ Reconnect to Host (reconnect-remote) starts a new connection to the same host — explicitly a new one, never a restore of the dead session.

A preset is applied once, at spawn. A pane opened from one is an ordinary pane afterwards: there is nothing to leave and nothing to keep in sync. Presets deliberately carry no colours, fonts or keybindings — those are window- or app-wide in Corta (DESIGN.md §6), and a per-preset copy would be a second settings store arguing with this file.


5. Keyboard shortcuts

bind.<command> = cmd+shift+d
bind.<command> =                 # an empty value unbinds

Modifiers: cmd/command, ctrl/control, alt/opt/option, shift. Joined to the key with +, case-insensitive, in any order — Corta writes a shortcut back as ctrl+alt+shift+cmd+key, which is the spelling the table below uses.

Keys: any single character (d, ,, =, +), or one of the named keys up, down, left, right, home, end, pageup, pagedown, return, enter, tab, space, escape, delete.

Unbinding is not the same as restoring the default: an empty value leaves the command with no key, which is what you want when a TUI needs one Corta was taking.

An unbound keystroke is passed to the child process, encoded like any other key Corta does not claim — it is not swallowed. That is the point of unbinding: bind.close = is how ⌘W stops closing the pane and starts reaching the program running in it. It also means unbinding is uniform — a key Corta never bound and a key you unbound behave identically — and that nothing can end up doing nothing, which reads as a bug rather than as a setting. The command itself stays reachable from its menu and from the command palette (⇧⌘P); Help ▸ Keyboard Shortcuts lists it as Not Set.

Nothing rejects two commands sharing one keystroke. Where AppKit decides, the first matching menu item in menu-bar order wins; where Corta decides, the first command in the table below wins. Help ▸ Keyboard Shortcuts shows the key against both rows, which is how you spot it.

The commands

bind. keyCommandDefault
new-windowNew Windowcmd+n
new-tabNew Tabcmd+t
previous-tabPrevious Tabshift+cmd+[
next-tabNext Tabshift+cmd+]
rename-tabRename Tab…(none)
select-tab-1Show Tab 1cmd+1
select-tab-2Show Tab 2cmd+2
select-tab-3Show Tab 3cmd+3
select-tab-4Show Tab 4cmd+4
select-tab-5Show Tab 5cmd+5
select-tab-6Show Tab 6cmd+6
select-tab-7Show Tab 7cmd+7
select-tab-8Show Tab 8cmd+8
select-last-tabShow Last Tabcmd+9
closeClosecmd+w
split-rightSplit Pane Rightcmd+d
split-downSplit Pane Downshift+cmd+d
focus-leftMove Focus Leftalt+cmd+left
focus-rightMove Focus Rightalt+cmd+right
focus-upMove Focus Upalt+cmd+up
focus-downMove Focus Downalt+cmd+down
grow-pane-horizontallyGrow Pane Horizontallyctrl+cmd+right
shrink-pane-horizontallyShrink Pane Horizontallyctrl+cmd+left
grow-pane-verticallyGrow Pane Verticallyctrl+cmd+down
shrink-pane-verticallyShrink Pane Verticallyctrl+cmd+up
zoom-paneZoom Paneshift+cmd+return
reopen-closed-paneReopen Closed Paneshift+cmd+t
equalize-panesEqualize Panes(none)
increase-font-sizeBiggercmd+=
decrease-font-sizeSmallercmd+-
reset-font-sizeActual Sizecmd+0
findFind…cmd+f
copyCopycmd+c
pastePastecmd+v
select-allSelect Allcmd+a
scroll-page-upScroll Page Upshift+pageup
scroll-page-downScroll Page Downshift+pagedown
scroll-to-topScroll to Topshift+home
scroll-to-bottomScroll to Bottomshift+end
previous-failed-commandPrevious Failed Commandshift+cmd+up
next-failed-commandNext Failed Commandshift+cmd+down
copy-last-command-outputCopy Last Command Output(none)
snapshot-running-command-outputSnapshot Running Command's Output(none)
export-command-outputExport Command Output…(none)
open-file-reference-in-commandOpen File Reference in Command Output(none)
search-command-historySearch Command History…(none)
reveal-working-directoryReveal Working Directory in Finder(none)
copy-working-directory-pathCopy Working Directory Path(none)
change-directory-to-parentChange Directory to Parent(none)
change-directory-to-project-rootChange Directory to Project Root(none)
open-parent-directory-in-new-paneOpen Parent Directory in New Pane(none)
open-project-root-in-new-paneOpen Project Root in New Pane(none)
export-textExport Text…shift+cmd+s
clear-screenClear Screencmd+k
clear-historyClear History(none)
reset-terminalReset Terminal(none)
reconnect-remoteReconnect to Host(none)
browse-remote-filesBrowse Remote Files…(none)
previous-commandPrevious Commandcmd+up
next-commandNext Commandcmd+down
secure-keyboard-entrySecure Keyboard Entry(none)
settingsSettings…cmd+,
command-paletteCommand Palette…shift+cmd+p

previous-command, next-command, the two failed-command jumps, copy-last-command-output, snapshot-running-command-output, export-command-output and open-file-reference-in-command all need shell integration (OSC 133) to have anything to work with; without it their menu items are disabled rather than silent, and search-command-history opens a window with nothing in it. copy-last-command-output uses the OSC 133 ; C output-start mark and the next prompt boundary, so a multiline prompt or continued command is excluded from the output. Integration that emits no C mark falls back to one row after the prompt; only that fallback can include part of a multiline prompt or command.

snapshot-running-command-output is copy-last-command-output's answer for a command that has not finished yet: copy-last-command-output needs the OSC 133 ; D mark that only arrives at the end, so it finds nothing while something is still building. This instead copies everything the running command has printed so far, with a header naming when the snapshot was taken — because what a build has printed at minute three is still worth reading, and waiting for it to finish to read it would be Corta making the user wait on itself.

export-command-output is copy-last-command-output written to a file instead of the clipboard, for a build log too long to want pasted anywhere but still worth attaching to a bug report. open-file-reference-in-command opens the last path:line[:column] reference in that command's output through open-file-command (§2), without hunting through the scrollback for it by eye. Both act on the command a jump (or a notification's click) landed on, else the one whose prompt is nearest the top of the viewport, else the last one that finished. search-command-history opens the Command History window for the pane — every recorded command with its time, exit status and directory, searchable, with Fill and Run for a command whose text is still in the scrollback (command-history-limit bounds how many are kept). Run only executes a complete visible single-line command of at most 80 characters without tabs; other commands are filled at the prompt for you to inspect and submit with Return.

The six working-directory commands act on the directory the pane's shell last reported through OSC 7, and are disabled until it has reported one. reveal-working-directory selects it in Finder and copy-working-directory-path puts the path on the clipboard. change-directory-to-parent and change-directory-to-project-root write a cd to the shell under the same safety gate every app-initiated directory change uses (§2, History): only when no command is running and the prompt is empty. The project root is the nearest ancestor containing .git. The two project-root commands look for it after you choose them — off the main thread, since the path is the shell's and may sit on a slow or unreachable volume — and show "No project root" when there is none, rather than being disabled ahead of time. open-parent-directory-in-new-pane and open-project-root-in-new-pane split the pane with a new local one rooted there instead. In a remote pane only change-directory-to-parent works — the cd goes to the remote shell, where the path lives — while the Finder, clipboard, project-root and new-pane commands stay disabled, because none of them may act on a path that belongs to another machine (SECURITY.md §2.8).

reopen-closed-pane puts a closed pane back where it was — same split, same side, same divider, same working directory. It restores the arrangement, never the process: the child that was running is gone, and its scrollback with it. The record is one pane deep, because the position of anything older is described against a tree the first reopen has already changed.

reconnect-remote appears only for a pane whose recorded spawn command was a remote launcher (ssh, mosh) and whose child has exited. It re-runs exactly that command — no fallback ladder, because landing in a silent local shell when you asked for the host back would look like success and be nothing of the kind. It is a new connection: the copy says so, and never suggests the dead remote shell or its scrollback came back. When the recorded command itself attaches to a remote multiplexer (tmux attach, screen -r), the copy says the command reattaches to whatever session exists on the host — the reattach is the remote program's doing, not Corta's.

browse-remote-files opens the SFTP browser for the focused pane's host — enabled only for a pane known to be remote. The host name comes from the remote shell's own report, which is program output, so the first connection to a host in each run is a question: the browser opens with the name prefilled and editable (correct it to the alias you would pass to ssh), and connects only when you press Connect; a ⌘-clicked file reference in a remote pane asks the same question once. The channel is the system's ssh -s -- <host> sftp, so authentication and ~/.ssh/config belong to OpenSSH; there is nothing to configure here. Like sftp(1) and scp(1), the channel never forwards the agent, X11 or ports, never runs a LocalCommand or a RemoteCommand, never asks for a remote terminal and never becomes a ControlMaster, whatever the configuration says for that host (it still reuses a master your terminal opened). It has no terminal, so ssh asks for a password, a key passphrase or a new host key through Corta, in a dialog; nothing is saved. (CORTA_SFTP_SSH, an environment variable and never a config key, names an absolute path to run instead of /usr/bin/ssh with the same argv — a verification hook for driving a Debug build only against a local sftp-server; Release and Benchmark ignore it. See environment switches.) Transfers use a .corta-part partial committed to the destination on completion, resumable with both endpoints re-validated, and every overwrite is a decision you make in a sheet, not a default. A remote file's Edit action — or ⌘-clicking a path:line reference in a remote pane — downloads a managed copy, opens it through open-file-command, and treats uploading your edits back as an explicit question, re-checking the remote for changes first.

export-text writes the selection — or, with nothing selected, the whole scrollback and screen — to a text file. It is the same text ⌘C would put on the clipboard, including how a soft-wrapped line is joined.

zoom-pane fills the window with the focused pane and the same key puts the split back. It is temporary and changes nothing: no pane is closed, no child process is disturbed, and the saved arrangement still describes the splits, not the zoom. The menu item names whichever direction it will go next.

The three terminal-state commands are separate because no two terminals mean the same thing by "clear", and each says what it discards:

CommandScreenScrollbackModes, colours, cursor
Clear Screenerased; ready shell editor keptkeptkept
Clear Historykeptdiscardedkept
Reset Terminaleraseddiscardedreset

At a ready integrated prompt, clear-screen relocates the complete displayed prompt and input to the top, preserving the cursor position within that input, cell attributes and wrapping. It needs this prompt's end marker (OSC 133 B); there is no prompt-text guessing or synthetic Return/Control-L. Without a ready prompt it retains the ordinary erase-and-home behavior. On the alternate screen it does nothing; an editor whose prompt starts in scrollback is also left intact. Scrollback is never appended or discarded by this command.

All three act on Corta's own grid, not on the program running in it: nothing is written to the child's input, so a running job is undisturbed and redraws on its next frame. clear-history and reset-terminal ship unbound — both throw history away, and a key that discards a build log by accident is not a default — and both ask before discarding, naming how many lines are at stake. The question is skipped when the scrollback is empty, and suppressed entirely by confirm-close = false, which is the existing key for "ask me before I lose work".

Every command in this table is also in the command palette (⇧⌘P), which lists the same table — so a command with no default binding is still one search away.


6. What is deliberately not configurable

  • TERM. Always xterm-256color (DECISIONS.md D08).
  • Reading the clipboard from the terminal. The OSC 52 read form is not implemented and will not be (SECURITY.md §6).
  • Title queries. Terminal programs cannot read back the window title. Colour queries are supported: OSC 4/5 report indexed/special colours, and OSC 10/11/12 report foreground, background and cursor colours. These are protocol responses, not settings switches.
  • Transparency, ligatures, and per-profile settings. Not shipped; each needs a decision in DECISIONS.md before it is a feature — propose one in Discussions → Ideas.

7. Where the values are applied

ChangeTakes effect
theme, appearance, font-family, font-family-cjk, font-weight, line-spacing, font-fallback, emoji-rendering, cursor-shape, cursor-blinkImmediately, in every open pane; a change to the cell's size refits the rows and columns to the window.
ambiguous-widthImmediately, for output written afterwards; text already on screen keeps its width.
font-sizeImmediately in panes without temporary font zoom; zoomed windows keep their temporary size until Actual Size (⌘0). New windows use the configured size.
bell, option-as-meta, search-case-sensitive, search-regex, open-file-command, paste-warning, copy-on-select, mouse-override-modifier, link-activation, allow-clipboard-write, confirm-close, directory-history, notification keysImmediately — they are read when the behaviour happens.
status-bar, status-items, status-network-interface, input-source indicator keysImmediately; polling follows visible enabled bars.
command-status-marksImmediately; changes the command-outcome overlay without changing terminal text.
directory-completion, history-suggestionsDisabling immediately hides suggestions and restores native key handling. Enabling requires a new shell to load automatic local zsh hooks; a live shell keeps hooks it already loaded.
bind.*Immediately: the menu key equivalents are re-applied on every file change.
theme.*Immediately, if the live theme is the one you edited.
columns, rowsThe next window opened.
scrollback-linesSessions started afterwards; a running shell keeps the history it has.
command-history-limitSessions started afterwards, same reason as scrollback-lines.
restore-windowsThe next launch.
secure-keyboard-entryImmediately, whenever a terminal window is key.
update-auto-checkImmediately — applied to the live Sparkle updater on every file change.
suggest-applications-folderThe next launch.

8. Config file vs. app-owned state

This file holds settings: values a person chose and might reasonably hand-edit, review in version control, or copy to another machine. Files under ~/Library/Application Support/Corta/ hold app-owned state, history and managed remote-edit copies. Their contents are not configuration keys.

StoreFileWhat it holdsCategory
Configuration~/.config/corta/config (this file)Appearance, terminal behaviour, notifications, shell integration and history toggles, updatesGlobal preference — applies to every window and pane until changed again
Configuration.columns/.rowssame fileThe grid a new window opens withWindow-launch default — read once, at the moment a window is created; changing it does nothing to windows already open
Configuration.presetssame fileNamed shell/directory/environment combinationsPane-launch configuration — read once, at the moment a pane spawns from that preset; never re-applied afterwards (docs/CONFIGURATION.md §4a)
SessionRestorestate.jsonWindow frames and split arrangement, restored on the next launchWindow/session state — describes this run's windows, not a preference about future ones
DirectoryHistoryStoredirectory-history.jsonRanked recent directoriesTemporary interaction history — gated by directory-history; clear it in Privacy & Security ▸ History
RecentHostsStorerecent-hosts.jsonRecently connected SSH/SFTP hostsConnection history — clear it in Privacy & Security ▸ Recent hosts
RemoteEditStoreRemoteEdit/manifest.json and managed copies beneath RemoteEdit/Remote baselines, local edits and approved-upload snapshotsManaged editing state — preserve undecided edits before deleting app data; used copies are pruned after 30 days unless edits are pending

Why separate stores: Configuration is line-oriented text specifically so a person can open it in $EDITOR, diff it, and understand every line; state.json and directory-history.json are JSON blobs a person never opens, rewritten wholesale on every change, with no comments or ordering worth preserving. Folding window arrangement or directory history into the hand-edited file would make every window move or cd churn the file a person is supposed to be able to read at a glance — the same reason SessionRestore.swift's own header comment gives for keeping state in Application Support rather than the config directory.

Both state.json and directory-history.json carry a version field (added in B09) so a future, incompatible change to either format can skip or migrate old data instead of the previous behaviour — silently discarding the entire file the moment it fails to decode. Configuration has never needed one: unknown keys are preserved verbatim (§1) and an unparsed line is simply skipped, so it already degrades one line at a time rather than as a whole file.

What each of these files may contain, and the modes they are written with, is the data-at-rest policy.

9. Local system status

CPU is the busy fraction of Mach CPU tick deltas between samples. Load shows 1-, 5-, and 15-minute averages. Memory is wired + non-purgeable anonymous + physical compressed pages, excluding file cache; it is not total minus free. Network rates use byte-counter deltas for exactly one interface, resetting the baseline on interface changes or pauses. Disk free is the available capacity of the volume containing the user's home directory, refreshed every 30 seconds. Thermal state is macOS's normal / elevated / high / critical classification, not a temperature in degrees. Metrics sample every two seconds off the UI thread, and all windows share one sampler. These are local-machine metrics, including while the selected pane is connected over SSH.

The bottom bar is 24 points high. It uses abbreviated labels and byte quantities with up to one decimal digit to fit more selected metrics. Hover or open details for full labels and precision; accessibility also receives the full text. Numbers follow the current locale, and labels follow the app's selected language. Host details are also available from View → Local host details or Settings → Terminal → Local host details, even with the status bar disabled.