docs(readme): rewrite README, split reference docs
Lead with vev's strengths (resilient UDP remote attach, server-side rendering, near-stdlib dependency footprint, perf harness) and move the configuration and terminal reference material to docs/. Fix inaccuracies found while verifying against the code: bare vev always creates an ephemeral session, kill --daemon behaves like --all on the wire, code.rename-tab=RNT was missing from the config example, and the offline recovery state re-runs the SSH bootstrap instead of resuming over SSH stdio. Document the default UDP port range in remote-resilience.md and the AZERTY top-row aliases.
- commit
- abde31ced0b1e1666472adc3bebe10506c644d77
- author
- brice · · 2026-07-14 14:11 UTC
- parent
- 20dcfd4
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +139 -216
M README.md +39 -214
| @@ -1,251 +1,76 @@ | |||
| 1 | 1 | # vev | |
| 2 | - | ***Norwegian: web, weave, tissue*** | |
| 3 | 2 | ||
| 4 | - | There are many terminal multiplexers, but this one is mine. Minimal aesthetic, purely focused on performance, rendering high-output of vertical text, agentic coding, and remote work over SSH. Built in Go with no external deps as much as possible. | |
| 3 | + | *Norwegian: to weave* | |
| 5 | 4 | ||
| 6 | - | > [!IMPORTANT] | |
| 7 | - | > This is totally in alpha. | |
| 5 | + | A terminal multiplexer for Linux. One Go binary, a per-user daemon, a thin client. | |
| 8 | 6 | ||
| 9 | - | ## Install | |
| 7 | + | > [!WARNING] | |
| 8 | + | > Alpha software, Linux only. Expect breaking changes. | |
| 10 | 9 | ||
| 11 | - | ```sh | |
| 12 | - | go install github.com/bnema/vev@latest | |
| 13 | - | ``` | |
| 10 | + | What it is built around: | |
| 14 | 11 | ||
| 15 | - | From a checkout: | |
| 12 | + | - **Remote attach that survives bad networks.** SSH bootstraps the connection, then the session runs over UDP and resumes after sleep or Wi-Fi changes. Inspired by mosh, with its own protocol. | |
| 13 | + | - **Server-side rendering.** The daemon keeps a full VT screen per pane and sends minimal diffs to the client, so high-output programs (agents, build logs) stay smooth locally and over SSH. | |
| 14 | + | - **Nothing but the standard library.** VT emulation, renderer, wire protocol, and UDP transport are all in this repo. The only runtime dependencies are `golang.org/x/sys` and `golang.org/x/term`. | |
| 15 | + | - **Performance treated as a feature.** A reproducible harness measures 252 scenarios across workloads and transports, including lossy and high-latency UDP. | |
| 16 | + | ||
| 17 | + | ## Install | |
| 16 | 18 | ||
| 17 | 19 | ```sh | |
| 18 | - | make install # install vev and default bar scripts to Go's bin directory | |
| 19 | - | go build -o vev . # local binary | |
| 20 | + | go install github.com/bnema/vev@latest | |
| 20 | 21 | ``` | |
| 21 | 22 | ||
| 22 | - | `make install` places `vev-bar-top-right` and `vev-bar-bottom-right` beside the installed `vev` binary. If you install another way, copy the scripts from `scripts/` into a directory on `PATH` or replace the bar commands in config. | |
| 23 | + | From a checkout, `make install` also installs the default status bar scripts. | |
| 23 | 24 | ||
| 24 | 25 | ## Usage | |
| 25 | 26 | ||
| 26 | 27 | ```text | |
| 27 | - | vev attach to (or create) an ephemeral session | |
| 28 | - | vev new <name> create and attach to a named session | |
| 29 | - | vev attach <name> attach to an existing session (alias: a) | |
| 30 | - | vev attach user@host[:session] attach to a remote vev daemon over SSH | |
| 31 | - | vev ls list sessions (alias: list) | |
| 32 | - | vev kill <name> kill a session | |
| 33 | - | vev kill -- <name> kill a name that starts with - | |
| 34 | - | vev kill --all kill all sessions and stop the daemon | |
| 35 | - | vev kill --daemon stop the active vev daemon | |
| 36 | - | vev --help show help (aliases: -h, help) | |
| 37 | - | vev --version show version (alias: version) | |
| 38 | - | ``` | |
| 39 | - | ||
| 40 | - | Ephemeral sessions get a number (`0`, `1`, `2`, ...) and are temporary daemon-retained sessions. They survive detach and transient client connection loss while the daemon is running, but they are not persisted to disk and disappear when the daemon is killed, exits, or the machine restarts. Named sessions are persisted, survive daemon restarts, and can be resumed later. New and renamed session names must start with a letter or number and may contain only letters, numbers, dots, underscores, and dashes. | |
| 41 | - | ||
| 42 | - | The daemon starts on first use and exits when no sessions remain. Only one client is attached to a session at a time; attaching again displaces the old client. | |
| 43 | - | ||
| 44 | - | ## Configuration | |
| 45 | - | ||
| 46 | - | vev reads `$XDG_CONFIG_HOME/vev/config`, or `~/.config/vev/config` when `XDG_CONFIG_HOME` is unset. Missing files use defaults. The daemon reloads mtime/size changes while running, polling about every two seconds. | |
| 47 | - | ||
| 48 | - | ```text | |
| 49 | - | # Theme: auto follows the client; dark/light force built-in palettes. | |
| 50 | - | theme = auto | |
| 51 | - | ||
| 52 | - | # Palette placement: auto, center, top-left, top, top-right, left, right, | |
| 53 | - | # bottom-left, bottom, or bottom-right. | |
| 54 | - | palette.anchor = auto | |
| 55 | - | ||
| 56 | - | # One prewarmed floating terminal per tab. An empty command uses the normal shell. | |
| 57 | - | floating.command = | |
| 58 | - | floating.width = 80% | |
| 59 | - | floating.height = 80% | |
| 60 | - | ||
| 61 | - | # Include the focused pane's OSC terminal title in tab labels (top bar and | |
| 62 | - | # session picker). The process name always shows; off keeps only that. | |
| 63 | - | tabs.terminal-title = on | |
| 64 | - | ||
| 65 | - | # Rebindable actions. Leave a line out to keep its built-in binding. | |
| 66 | - | open-palette = alt+space | |
| 67 | - | toggle-floating-pane = alt+f | |
| 68 | - | jump-attention = alt+a | |
| 69 | - | focus-pane-left = alt+h | |
| 70 | - | focus-pane-right = alt+l | |
| 71 | - | focus-pane-up = alt+k | |
| 72 | - | focus-pane-down = alt+j | |
| 73 | - | switch-tab-1 = alt+1 | |
| 74 | - | switch-tab-2 = alt+2 | |
| 75 | - | switch-tab-3 = alt+3 | |
| 76 | - | switch-tab-4 = alt+4 | |
| 77 | - | switch-tab-5 = alt+5 | |
| 78 | - | switch-tab-6 = alt+6 | |
| 79 | - | switch-tab-7 = alt+7 | |
| 80 | - | switch-tab-8 = alt+8 | |
| 81 | - | switch-tab-9 = alt+9 | |
| 82 | - | ||
| 83 | - | # Named session snapshots restore layout, cwd, visible content, scrollback, | |
| 84 | - | # and foreground processes from this allowlist. Omit the key for these defaults; | |
| 85 | - | # set it empty to disable process relaunch. | |
| 86 | - | snapshot.restore_processes = vi,vim,nvim,emacs,man,less,more,tail,top,htop,btop,claude,codex,pi,opencode | |
| 87 | - | ||
| 88 | - | # Right bar anchors. Empty command values disable the matching anchor. | |
| 89 | - | bar.top-right = vev-bar-top-right | |
| 90 | - | bar.bottom-right = vev-bar-bottom-right | |
| 91 | - | bar.interval = 5s | |
| 92 | - | ||
| 93 | - | # Command palette codes are trimmed, uppercased, then checked as 2-3 letters or digits. | |
| 94 | - | code.new-tab = CNT | |
| 95 | - | code.new-session = CNS | |
| 96 | - | code.close-tab = CLT | |
| 97 | - | code.split-right = SPR | |
| 98 | - | code.split-left = SPL | |
| 99 | - | code.split-up = SPU | |
| 100 | - | code.split-down = SPD | |
| 101 | - | code.stack-pane = STP | |
| 102 | - | code.toggle-stack = TST | |
| 103 | - | code.close-pane = CLP | |
| 104 | - | code.focus-pane-left = FPL | |
| 105 | - | code.focus-pane-right = FPR | |
| 106 | - | code.focus-pane-up = FPU | |
| 107 | - | code.focus-pane-down = FPD | |
| 108 | - | code.next-tab = NXT | |
| 109 | - | code.previous-tab = PVT | |
| 110 | - | code.back-session = BSK | |
| 111 | - | code.jump-recent-session = JRS | |
| 112 | - | code.session-picker = SSP | |
| 113 | - | code.visual-mode = VIS | |
| 114 | - | code.toggle-floating-pane = FLT | |
| 115 | - | code.rename-session = RNS | |
| 116 | - | code.detach = DET | |
| 117 | - | ``` | |
| 118 | - | ||
| 119 | - | Configuring an action replaces all of its built-in aliases. In the example above, the focus-pane lines keep Alt+h/j/k/l but not the Alt+Arrow aliases; omit those lines to keep all built-ins. | |
| 120 | - | ||
| 121 | - | Key specs support `alt+<char>`, `alt+space`, and `alt+left/right/up/down`; digit key specs support `alt+1` through `alt+9`, not `alt+0`. `jump-attention` first opens the oldest attention tab in the current session; if none exists, it opens the oldest attention tab in another session. Invalid entries are logged as warnings and skipped where possible; duplicate config keys use the last value, binding conflicts keep the later action's defaults, and command-code conflicts drop the conflicting override. | |
| 122 | - | ||
| 123 | - | `floating.command` runs through the normal shell; leave it empty to launch that shell directly. `floating.width` and `floating.height` require exact percentages from `1%` through `100%`; invalid values log a warning and leave the last accepted value (or the `80%` default) intact. Reloading does not relaunch or immediately alter an installed floating terminal: a changed command is used on its next launch, while changed dimensions take effect when that terminal is next shown or resized. Only configuration—not floating runtime state—is restored after a daemon restart; restored tabs stay cold until first visited. | |
| 124 | - | ||
| 125 | - | `tabs.terminal-title` accepts `on` or `off` and defaults to `on`; invalid values log a warning and keep the last accepted value. Reload applies a changed value live. | |
| 126 | - | ||
| 127 | - | ### Command palette anchoring | |
| 128 | - | ||
| 129 | - | `palette.anchor` accepts `auto` or the nine anchors `center`, `top-left`, `top`, `top-right`, `left`, `right`, `bottom-left`, `bottom`, and `bottom-right`. Values are trimmed and case-insensitive. The default is `auto`: palettes use the bottom shelf when the terminal is narrower than 96 columns, and a 64-column bottom-right rail at 96 columns or wider. An explicit anchor changes placement only; palette query and selection are preserved. Invalid values are warned about and leave the last valid value unchanged, as do duplicate entries until a valid replacement is supplied (the last valid value wins). Configuration reload applies a changed anchor live and repositions an open palette without resetting its query or selection. | |
| 130 | - | ||
| 131 | - | ### Bar right anchors | |
| 132 | - | ||
| 133 | - | The top and bottom bar right anchors run configurable commands on the daemon host. By default, `bar.top-right = vev-bar-top-right` prints CPU, memory, then hour text (for example `C 12% M 48% 09:41`), and `bar.bottom-right = vev-bar-bottom-right` prints compact git state for the focused pane's `VEV_PANE_CWD`. Set either value empty to disable that anchor, or set it to another command to replace it. | |
| 134 | - | ||
| 135 | - | Both anchors share `bar.interval`, which defaults to `5s`; values below `1s` are clamped to `1s`. vev also refreshes around attach, resize, focus, and copy-feedback changes. Commands are bounded by a timeout and output limit. On failure or timeout, vev keeps the last good value; if there is none, the anchor is empty. | |
| 136 | - | ||
| 137 | - | Scripts receive these environment variables when available: `VEV_ANCHOR` (`top-right` or `bottom-right`), `VEV_SESSION`, `VEV_TAB`, `VEV_PANE`, `VEV_PANE_CWD`, and `VEV_COLS`. vev reads stdout only, uses the first line only, treats it as plain UTF-8 display text, and strips ANSI escape sequences and other control characters. Unicode and Nerd Font glyphs are supported. The v1 API does not support colors, styles, JSON, streaming output, or per-anchor intervals. The default git script runs `git status --porcelain` each refresh, so increase `bar.interval` or replace the command if that is too expensive for a large repository. | |
| 138 | - | ||
| 139 | - | ## Remote attach | |
| 140 | - | ||
| 141 | - | ```sh | |
| 142 | - | vev attach user@host | |
| 143 | - | vev attach user@host:session | |
| 28 | + | vev create an ephemeral session | |
| 29 | + | vev new <name> create a named session | |
| 30 | + | vev attach <name> attach to a session (alias: a) | |
| 31 | + | vev attach user@host[:session] attach to a remote daemon over SSH | |
| 32 | + | vev ls list sessions | |
| 33 | + | vev kill <name> kill a session (--all kills everything) | |
| 144 | 34 | ``` | |
| 145 | 35 | ||
| 146 | - | Remote attach uses SSH only to start a UDP proxy on the remote host; the session itself then talks directly to the remote host over UDP. `vev` must be installed on the remote host, and the host name you pass to `vev attach` must be reachable both by SSH for bootstrap and by UDP for the session transport. Omitting `:session` opens an ephemeral remote session. Remote sessions can resume after temporary network disconnects, such as sleep or Wi-Fi changes, while the daemon still has the session. Named sessions additionally persist across daemon restarts. | |
| 147 | - | ||
| 148 | - | vev's resilient remote attach work is inspired by [mosh](https://mosh.org/)'s mobile-shell approach to UDP-based recovery across flaky networks. vev uses its own wire protocol and implementation; it is not compatible with mosh clients or servers and does not copy mosh protocol code. | |
| 149 | - | ||
| 150 | - | The default remote transport uses UDP. Its proxy binds a port in a fixed range on the remote (default `61000-61023`), so a host firewall that only allows SSH will drop the session datagrams and attach fails with `remote UDP transport unavailable: probe UDP transport`. Allow inbound UDP for that range on the remote to fix it — for example `sudo ufw allow 61000:61023/udp`, or on a trusted Tailscale network `sudo ufw allow in on tailscale0`. Override the range with `VEV_UDP_PORT_RANGE` on the remote (e.g. `VEV_UDP_PORT_RANGE=51000-51009`, a single `VEV_UDP_PORT_RANGE=51000`, or `VEV_UDP_PORT_RANGE=0` for a random ephemeral port). | |
| 151 | - | ||
| 152 | - | Set `VEV_REMOTE_TRANSPORT=stdio` to use SSH stdio compatibility mode on networks where direct UDP is blocked or the SSH target is only reachable through jump-host or SSH-config-only routing. Stdio mode keeps all traffic inside SSH and does not require inbound UDP, but it may not notice sleep or Wi-Fi loss promptly. See `docs/remote-resilience.md` for concise operational notes. | |
| 153 | - | ||
| 154 | - | ## Terminal color | |
| 155 | - | ||
| 156 | - | When the attaching client reports truecolor support, vev advertises newly spawned panes as `TERM=xterm-direct` and exports `COLORTERM=truecolor` to child processes. Otherwise, panes use the conservative `TERM=xterm-256color` and do not receive `COLORTERM`. This works for local and remote attach because the client capability is carried inside vev's protocol rather than relying on SSH `SendEnv`/`AcceptEnv`. | |
| 157 | - | ||
| 158 | - | Applications that use terminfo can detect direct color from `xterm-direct`; applications that use the common environment convention can use `COLORTERM=truecolor` when it is present. Panes restored from a saved snapshot start conservatively before any client attaches; after attach or resume, newly spawned panes use that client's reported capability. | |
| 159 | - | ||
| 160 | - | ## Terminal compatibility | |
| 161 | - | ||
| 162 | - | vev keeps a server-side VT screen for each pane. It tracks scroll regions with DECOM origin mode, ANSI insert mode, cursor-position and mode-query reports, alternate-screen state, bracketed paste, mouse tracking, synchronized updates, OSC 9/777 notifications, OSC 52 clipboard set requests, and double-width cells. Resize and edit operations repair double-width cells so CJK or emoji glyph halves are not left behind. | |
| 36 | + | The daemon starts on first use and exits with the last session. Ephemeral sessions are numbered, survive detach, and disappear with the daemon. Named sessions persist across daemon restarts and come back with their layout, scrollback, and allowlisted processes. | |
| 163 | 37 | ||
| 164 | 38 | ## Keys | |
| 165 | 39 | ||
| 166 | - | All bindings use Alt directly; there is no prefix key. | |
| 167 | - | ||
| 168 | - | | Key | Action | | |
| 169 | - | |---|---| | |
| 170 | - | | Alt+Space | open the command palette | | |
| 171 | - | | Alt+f | show or hide the current tab's floating terminal | | |
| 172 | - | | Alt+1 .. Alt+9 | switch to tab by number | | |
| 173 | - | | Alt+h/j/k/l | focus pane left/down/up/right | | |
| 174 | - | | Alt+Arrow | focus pane by direction | | |
| 175 | - | | Alt+a | jump to a session with attention | | |
| 176 | - | ||
| 177 | - | Some terminals encode Alt+Space as a non-breaking space instead of `ESC Space`; in those terminals, the palette binding depends on whether the terminal can send `ESC Space` for Alt+Space. | |
| 178 | - | ||
| 179 | - | ## Command palette | |
| 180 | - | ||
| 181 | - | Open the palette with Alt+Space. Type a command code or fuzzy-match the label/description. Use Up/Down or Ctrl-N/Ctrl-P to move, Tab to complete the selected command code, Enter to run, and Esc/Ctrl-C to close. With an empty query, Tab completes the most recently successful command; commands that take an argument complete with a trailing space and preserve an argument already entered. | |
| 182 | - | ||
| 183 | - | Successful commands are promoted near the top the next time the palette opens. `BSK` toggles back to your previously active session. Typing `JRS` lists recent sessions by number in the status bar, and `JRS N` jumps to number N; the numbers are captured when you start typing, so a busy MRU list never shifts under you. An invalid or stale number leaves the palette open. | |
| 184 | - | ||
| 185 | - | | Code | Action | | |
| 186 | - | |---|---| | |
| 187 | - | | CNT | create new tab | | |
| 188 | - | | CNS | create and switch to a named session | | |
| 189 | - | | CLT | close current tab | | |
| 190 | - | | SPR / SPL / SPU / SPD | split pane right / left / up / down | | |
| 191 | - | | STP | stack a new pane | | |
| 192 | - | | TST | toggle focused stack | | |
| 193 | - | | CLP | close focused pane | | |
| 194 | - | | FPL / FPR / FPU / FPD | focus pane left / right / up / down | | |
| 195 | - | | NXT / PVT | switch to next / previous tab | | |
| 196 | - | | BSK | toggle the previously active session | | |
| 197 | - | | JRS N | jump directly to recent session rank N | | |
| 198 | - | | SSP | open session picker | | |
| 199 | - | | VIS | enter visual mode | | |
| 200 | - | | FLT | show or hide the current tab's floating terminal | | |
| 201 | - | | RNS | rename session | | |
| 202 | - | | RNT | rename current tab | | |
| 203 | - | | DET | detach | | |
| 204 | - | ||
| 205 | - | Rename prompts are prefilled with the current session name. Empty names and names already used by another session are rejected. | |
| 206 | - | ||
| 207 | - | ## Session picker | |
| 208 | - | ||
| 209 | - | Open it with `SSP` in the palette. It lists sessions and tabs — tab entries use the same `name (title)` form as the top bar, ellipsized to the list width — previews the selected target when space allows, and marks stopped sessions. Wide pickers place the preview beside the list with a muted divider; narrower pickers stack it below the list, then hide it when neither layout has useful space. A clipped preview stays anchored to the bottom of the target's visible terminal frame and never loads its scrollback. | |
| 40 | + | No prefix key; everything is Alt. | |
| 210 | 41 | ||
| 211 | 42 | | Key | Action | | |
| 212 | 43 | |---|---| | |
| 213 | - | | j / k, Up / Down | move selection | | |
| 214 | - | | Enter | switch to or resume selected target | | |
| 215 | - | | x | kill/delete selected target | | |
| 216 | - | | q, Esc, Ctrl-C | close picker | | |
| 217 | - | ||
| 218 | - | ## Panes and tabs | |
| 44 | + | | Alt+Space | command palette | | |
| 45 | + | | Alt+f | floating terminal for the current tab | | |
| 46 | + | | Alt+1 .. 9 | switch tab | | |
| 47 | + | | Alt+h/j/k/l, Alt+Arrow | focus pane | | |
| 48 | + | | Alt+a | jump to a session needing attention | | |
| 219 | 49 | ||
| 220 | - | Tabs hold pane layouts. Panes can be split left/right/up/down, stacked, focused by direction, and closed from the command palette. Clicking a pane focuses it. | |
| 50 | + | The palette does the rest: type a short code (`SPR` split right, `CNT` new tab, `SSP` session picker, ...) or fuzzy-search the command list. Scroll up with the mouse to enter scrollback; vim keys move, `v` selects, `y` copies via OSC 52. | |
| 221 | 51 | ||
| 222 | - | Each tab also owns one independent floating terminal. It prewarms asynchronously on the tab's first visit, opens centered at the configured percentage of the pane-content area, and dims normal pane content while leaving status bars and popup chrome crisp. Hiding it retains the process, screen, and scrollback; switching tabs restores each tab's own visible or hidden state. Process exit clears the slot so the next open recreates it. The title combines the foreground process and OSC terminal title as `ProcessName: ProcessTerminalTitle`, falling back to either available title and then the configured command or shell. | |
| 52 | + | ## Remote attach | |
| 223 | 53 | ||
| 224 | - | While visible, the floating terminal receives ordinary input, bracketed paste, mouse input, clipboard-path injection, and scrollback/copy mode. Global vev bindings remain active, and a bare Esc is passed to the floating application. `Alt+f` and `FLT` toggle visibility; there is no separate floating-pane close command. | |
| 54 | + | ```sh | |
| 55 | + | vev attach user@host[:session] | |
| 56 | + | ``` | |
| 225 | 57 | ||
| 226 | - | The top bar shows each tab as `name (title)`, where the name is the tab's rename or number and the title is the focused pane's `ProcessName: ProcessTerminalTitle`. On truecolor clients the name renders bold and the parenthesized title slightly muted (in both the top bar and the session picker), derived from the client's reported theme; other terminals render plain labels. Set `tabs.terminal-title = off` to drop the `ProcessTerminalTitle` half from tab labels (top bar and session picker) and show only the process name; floating and stacked pane title bars are unaffected. Labels share the row width: when everything fits they render in full, otherwise unused space flows from short tabs to long ones and long labels are ellipsized, degrading to the bare tab name when space runs out. The bottom bar shows the active session followed by recent sessions, fading toward older entries. Ephemeral sessions are marked with `*`, for example `0*`. | |
| 58 | + | vev must be installed on the remote. If your firewall only allows SSH, open the UDP range first (default `61000:61023`, override with `VEV_UDP_PORT_RANGE`): | |
| 227 | 59 | ||
| 228 | - | ## Scrollback mode and copy | |
| 60 | + | ```sh | |
| 61 | + | sudo ufw allow 61000:61023/udp | |
| 62 | + | ``` | |
| 229 | 63 | ||
| 230 | - | Scrollback mode freezes a view over history while the program keeps running underneath. Enter it with the `VIS` palette command or by scrolling up with the mouse wheel. Passive browsing is labeled `[SCROLL]`; selecting lines changes the status to `[SELECT]`. Scrolling back to the bottom exits to the live pane. | |
| 64 | + | Where UDP is not an option, `VEV_REMOTE_TRANSPORT=stdio` keeps everything inside SSH at the cost of slower disconnect detection. Details in [docs/remote-resilience.md](docs/remote-resilience.md). | |
| 231 | 65 | ||
| 232 | - | | Key | Action | | |
| 233 | - | |---|---| | |
| 234 | - | | j / k, Up / Down | move one line | | |
| 235 | - | | PgUp / PgDn | move one page | | |
| 236 | - | | g / G | jump to top / bottom | | |
| 237 | - | | v or Space | toggle line selection | | |
| 238 | - | | y or Enter | copy selection and exit | | |
| 239 | - | | q, Esc, Ctrl-C | exit without copying | | |
| 66 | + | ## Configuration | |
| 240 | 67 | ||
| 241 | - | Copy uses OSC 52 clipboard sequences when supported by your terminal. After a successful copy, the bottom bar briefly shows `copied N chars to clipboard`. | |
| 68 | + | Optional file at `~/.config/vev/config`, reloaded live. Themes, key bindings, palette codes, the floating terminal, status bar scripts: see [docs/configuration.md](docs/configuration.md). Terminal color and VT compatibility notes live in [docs/terminal.md](docs/terminal.md). | |
| 242 | 69 | ||
| 243 | 70 | ## Development | |
| 244 | 71 | ||
| 245 | 72 | ```sh | |
| 246 | - | go install golang.org/x/tools/cmd/goimports@latest | |
| 247 | - | go install github.com/vektra/mockery/v2@latest | |
| 248 | 73 | make test # go test ./... -race | |
| 249 | - | make lint # goimports check, then go vet ./... | |
| 74 | + | make lint # goimports check, go vet | |
| 250 | 75 | make mocks # regenerate mocks | |
| 251 | 76 | ``` | |
| @@ -1,251 +1,76 @@ | |||||
| 1 | # vev | 1 | # vev | ||
| 2 | - | ***Norwegian: web, weave, tissue*** | |||
| 3 | 2 | ||||
| 4 | - | There are many terminal multiplexers, but this one is mine. Minimal aesthetic, purely focused on performance, rendering high-output of vertical text, agentic coding, and remote work over SSH. Built in Go with no external deps as much as possible. | 3 | + | *Norwegian: to weave* |
| 5 | 4 | ||||
| 6 | - | > [!IMPORTANT] | 5 | + | A terminal multiplexer for Linux. One Go binary, a per-user daemon, a thin client. |
| 7 | - | > This is totally in alpha. | |||
| 8 | 6 | ||||
| 9 | - | ## Install | 7 | + | > [!WARNING] |
| 8 | + | > Alpha software, Linux only. Expect breaking changes. | |||
| 10 | 9 | ||||
| 11 | - | ```sh | 10 | + | What it is built around: |
| 12 | - | go install github.com/bnema/vev@latest | |||
| 13 | - | ``` | |||
| 14 | 11 | ||||
| 15 | - | From a checkout: | 12 | + | - **Remote attach that survives bad networks.** SSH bootstraps the connection, then the session runs over UDP and resumes after sleep or Wi-Fi changes. Inspired by mosh, with its own protocol. |
| 13 | + | - **Server-side rendering.** The daemon keeps a full VT screen per pane and sends minimal diffs to the client, so high-output programs (agents, build logs) stay smooth locally and over SSH. | |||
| 14 | + | - **Nothing but the standard library.** VT emulation, renderer, wire protocol, and UDP transport are all in this repo. The only runtime dependencies are `golang.org/x/sys` and `golang.org/x/term`. | |||
| 15 | + | - **Performance treated as a feature.** A reproducible harness measures 252 scenarios across workloads and transports, including lossy and high-latency UDP. | |||
| 16 | + | ||||
| 17 | + | ## Install | |||
| 16 | 18 | ||||
| 17 | ```sh | 19 | ```sh | ||
| 18 | - | make install # install vev and default bar scripts to Go's bin directory | 20 | + | go install github.com/bnema/vev@latest |
| 19 | - | go build -o vev . # local binary | |||
| 20 | ``` | 21 | ``` | ||
| 21 | 22 | ||||
| 22 | - | `make install` places `vev-bar-top-right` and `vev-bar-bottom-right` beside the installed `vev` binary. If you install another way, copy the scripts from `scripts/` into a directory on `PATH` or replace the bar commands in config. | 23 | + | From a checkout, `make install` also installs the default status bar scripts. |
| 23 | 24 | ||||
| 24 | ## Usage | 25 | ## Usage | ||
| 25 | 26 | ||||
| 26 | ```text | 27 | ```text | ||
| 27 | - | vev attach to (or create) an ephemeral session | 28 | + | vev create an ephemeral session |
| 28 | - | vev new <name> create and attach to a named session | 29 | + | vev new <name> create a named session |
| 29 | - | vev attach <name> attach to an existing session (alias: a) | 30 | + | vev attach <name> attach to a session (alias: a) |
| 30 | - | vev attach user@host[:session] attach to a remote vev daemon over SSH | 31 | + | vev attach user@host[:session] attach to a remote daemon over SSH |
| 31 | - | vev ls list sessions (alias: list) | 32 | + | vev ls list sessions |
| 32 | - | vev kill <name> kill a session | 33 | + | vev kill <name> kill a session (--all kills everything) |
| 33 | - | vev kill -- <name> kill a name that starts with - | |||
| 34 | - | vev kill --all kill all sessions and stop the daemon | |||
| 35 | - | vev kill --daemon stop the active vev daemon | |||
| 36 | - | vev --help show help (aliases: -h, help) | |||
| 37 | - | vev --version show version (alias: version) | |||
| 38 | - | ``` | |||
| 39 | - | ||||
| 40 | - | Ephemeral sessions get a number (`0`, `1`, `2`, ...) and are temporary daemon-retained sessions. They survive detach and transient client connection loss while the daemon is running, but they are not persisted to disk and disappear when the daemon is killed, exits, or the machine restarts. Named sessions are persisted, survive daemon restarts, and can be resumed later. New and renamed session names must start with a letter or number and may contain only letters, numbers, dots, underscores, and dashes. | |||
| 41 | - | ||||
| 42 | - | The daemon starts on first use and exits when no sessions remain. Only one client is attached to a session at a time; attaching again displaces the old client. | |||
| 43 | - | ||||
| 44 | - | ## Configuration | |||
| 45 | - | ||||
| 46 | - | vev reads `$XDG_CONFIG_HOME/vev/config`, or `~/.config/vev/config` when `XDG_CONFIG_HOME` is unset. Missing files use defaults. The daemon reloads mtime/size changes while running, polling about every two seconds. | |||
| 47 | - | ||||
| 48 | - | ```text | |||
| 49 | - | # Theme: auto follows the client; dark/light force built-in palettes. | |||
| 50 | - | theme = auto | |||
| 51 | - | ||||
| 52 | - | # Palette placement: auto, center, top-left, top, top-right, left, right, | |||
| 53 | - | # bottom-left, bottom, or bottom-right. | |||
| 54 | - | palette.anchor = auto | |||
| 55 | - | ||||
| 56 | - | # One prewarmed floating terminal per tab. An empty command uses the normal shell. | |||
| 57 | - | floating.command = | |||
| 58 | - | floating.width = 80% | |||
| 59 | - | floating.height = 80% | |||
| 60 | - | ||||
| 61 | - | # Include the focused pane's OSC terminal title in tab labels (top bar and | |||
| 62 | - | # session picker). The process name always shows; off keeps only that. | |||
| 63 | - | tabs.terminal-title = on | |||
| 64 | - | ||||
| 65 | - | # Rebindable actions. Leave a line out to keep its built-in binding. | |||
| 66 | - | open-palette = alt+space | |||
| 67 | - | toggle-floating-pane = alt+f | |||
| 68 | - | jump-attention = alt+a | |||
| 69 | - | focus-pane-left = alt+h | |||
| 70 | - | focus-pane-right = alt+l | |||
| 71 | - | focus-pane-up = alt+k | |||
| 72 | - | focus-pane-down = alt+j | |||
| 73 | - | switch-tab-1 = alt+1 | |||
| 74 | - | switch-tab-2 = alt+2 | |||
| 75 | - | switch-tab-3 = alt+3 | |||
| 76 | - | switch-tab-4 = alt+4 | |||
| 77 | - | switch-tab-5 = alt+5 | |||
| 78 | - | switch-tab-6 = alt+6 | |||
| 79 | - | switch-tab-7 = alt+7 | |||
| 80 | - | switch-tab-8 = alt+8 | |||
| 81 | - | switch-tab-9 = alt+9 | |||
| 82 | - | ||||
| 83 | - | # Named session snapshots restore layout, cwd, visible content, scrollback, | |||
| 84 | - | # and foreground processes from this allowlist. Omit the key for these defaults; | |||
| 85 | - | # set it empty to disable process relaunch. | |||
| 86 | - | snapshot.restore_processes = vi,vim,nvim,emacs,man,less,more,tail,top,htop,btop,claude,codex,pi,opencode | |||
| 87 | - | ||||
| 88 | - | # Right bar anchors. Empty command values disable the matching anchor. | |||
| 89 | - | bar.top-right = vev-bar-top-right | |||
| 90 | - | bar.bottom-right = vev-bar-bottom-right | |||
| 91 | - | bar.interval = 5s | |||
| 92 | - | ||||
| 93 | - | # Command palette codes are trimmed, uppercased, then checked as 2-3 letters or digits. | |||
| 94 | - | code.new-tab = CNT | |||
| 95 | - | code.new-session = CNS | |||
| 96 | - | code.close-tab = CLT | |||
| 97 | - | code.split-right = SPR | |||
| 98 | - | code.split-left = SPL | |||
| 99 | - | code.split-up = SPU | |||
| 100 | - | code.split-down = SPD | |||
| 101 | - | code.stack-pane = STP | |||
| 102 | - | code.toggle-stack = TST | |||
| 103 | - | code.close-pane = CLP | |||
| 104 | - | code.focus-pane-left = FPL | |||
| 105 | - | code.focus-pane-right = FPR | |||
| 106 | - | code.focus-pane-up = FPU | |||
| 107 | - | code.focus-pane-down = FPD | |||
| 108 | - | code.next-tab = NXT | |||
| 109 | - | code.previous-tab = PVT | |||
| 110 | - | code.back-session = BSK | |||
| 111 | - | code.jump-recent-session = JRS | |||
| 112 | - | code.session-picker = SSP | |||
| 113 | - | code.visual-mode = VIS | |||
| 114 | - | code.toggle-floating-pane = FLT | |||
| 115 | - | code.rename-session = RNS | |||
| 116 | - | code.detach = DET | |||
| 117 | - | ``` | |||
| 118 | - | ||||
| 119 | - | Configuring an action replaces all of its built-in aliases. In the example above, the focus-pane lines keep Alt+h/j/k/l but not the Alt+Arrow aliases; omit those lines to keep all built-ins. | |||
| 120 | - | ||||
| 121 | - | Key specs support `alt+<char>`, `alt+space`, and `alt+left/right/up/down`; digit key specs support `alt+1` through `alt+9`, not `alt+0`. `jump-attention` first opens the oldest attention tab in the current session; if none exists, it opens the oldest attention tab in another session. Invalid entries are logged as warnings and skipped where possible; duplicate config keys use the last value, binding conflicts keep the later action's defaults, and command-code conflicts drop the conflicting override. | |||
| 122 | - | ||||
| 123 | - | `floating.command` runs through the normal shell; leave it empty to launch that shell directly. `floating.width` and `floating.height` require exact percentages from `1%` through `100%`; invalid values log a warning and leave the last accepted value (or the `80%` default) intact. Reloading does not relaunch or immediately alter an installed floating terminal: a changed command is used on its next launch, while changed dimensions take effect when that terminal is next shown or resized. Only configuration—not floating runtime state—is restored after a daemon restart; restored tabs stay cold until first visited. | |||
| 124 | - | ||||
| 125 | - | `tabs.terminal-title` accepts `on` or `off` and defaults to `on`; invalid values log a warning and keep the last accepted value. Reload applies a changed value live. | |||
| 126 | - | ||||
| 127 | - | ### Command palette anchoring | |||
| 128 | - | ||||
| 129 | - | `palette.anchor` accepts `auto` or the nine anchors `center`, `top-left`, `top`, `top-right`, `left`, `right`, `bottom-left`, `bottom`, and `bottom-right`. Values are trimmed and case-insensitive. The default is `auto`: palettes use the bottom shelf when the terminal is narrower than 96 columns, and a 64-column bottom-right rail at 96 columns or wider. An explicit anchor changes placement only; palette query and selection are preserved. Invalid values are warned about and leave the last valid value unchanged, as do duplicate entries until a valid replacement is supplied (the last valid value wins). Configuration reload applies a changed anchor live and repositions an open palette without resetting its query or selection. | |||
| 130 | - | ||||
| 131 | - | ### Bar right anchors | |||
| 132 | - | ||||
| 133 | - | The top and bottom bar right anchors run configurable commands on the daemon host. By default, `bar.top-right = vev-bar-top-right` prints CPU, memory, then hour text (for example `C 12% M 48% 09:41`), and `bar.bottom-right = vev-bar-bottom-right` prints compact git state for the focused pane's `VEV_PANE_CWD`. Set either value empty to disable that anchor, or set it to another command to replace it. | |||
| 134 | - | ||||
| 135 | - | Both anchors share `bar.interval`, which defaults to `5s`; values below `1s` are clamped to `1s`. vev also refreshes around attach, resize, focus, and copy-feedback changes. Commands are bounded by a timeout and output limit. On failure or timeout, vev keeps the last good value; if there is none, the anchor is empty. | |||
| 136 | - | ||||
| 137 | - | Scripts receive these environment variables when available: `VEV_ANCHOR` (`top-right` or `bottom-right`), `VEV_SESSION`, `VEV_TAB`, `VEV_PANE`, `VEV_PANE_CWD`, and `VEV_COLS`. vev reads stdout only, uses the first line only, treats it as plain UTF-8 display text, and strips ANSI escape sequences and other control characters. Unicode and Nerd Font glyphs are supported. The v1 API does not support colors, styles, JSON, streaming output, or per-anchor intervals. The default git script runs `git status --porcelain` each refresh, so increase `bar.interval` or replace the command if that is too expensive for a large repository. | |||
| 138 | - | ||||
| 139 | - | ## Remote attach | |||
| 140 | - | ||||
| 141 | - | ```sh | |||
| 142 | - | vev attach user@host | |||
| 143 | - | vev attach user@host:session | |||
| 144 | ``` | 34 | ``` | ||
| 145 | 35 | ||||
| 146 | - | Remote attach uses SSH only to start a UDP proxy on the remote host; the session itself then talks directly to the remote host over UDP. `vev` must be installed on the remote host, and the host name you pass to `vev attach` must be reachable both by SSH for bootstrap and by UDP for the session transport. Omitting `:session` opens an ephemeral remote session. Remote sessions can resume after temporary network disconnects, such as sleep or Wi-Fi changes, while the daemon still has the session. Named sessions additionally persist across daemon restarts. | 36 | + | The daemon starts on first use and exits with the last session. Ephemeral sessions are numbered, survive detach, and disappear with the daemon. Named sessions persist across daemon restarts and come back with their layout, scrollback, and allowlisted processes. |
| 147 | - | ||||
| 148 | - | vev's resilient remote attach work is inspired by [mosh](https://mosh.org/)'s mobile-shell approach to UDP-based recovery across flaky networks. vev uses its own wire protocol and implementation; it is not compatible with mosh clients or servers and does not copy mosh protocol code. | |||
| 149 | - | ||||
| 150 | - | The default remote transport uses UDP. Its proxy binds a port in a fixed range on the remote (default `61000-61023`), so a host firewall that only allows SSH will drop the session datagrams and attach fails with `remote UDP transport unavailable: probe UDP transport`. Allow inbound UDP for that range on the remote to fix it — for example `sudo ufw allow 61000:61023/udp`, or on a trusted Tailscale network `sudo ufw allow in on tailscale0`. Override the range with `VEV_UDP_PORT_RANGE` on the remote (e.g. `VEV_UDP_PORT_RANGE=51000-51009`, a single `VEV_UDP_PORT_RANGE=51000`, or `VEV_UDP_PORT_RANGE=0` for a random ephemeral port). | |||
| 151 | - | ||||
| 152 | - | Set `VEV_REMOTE_TRANSPORT=stdio` to use SSH stdio compatibility mode on networks where direct UDP is blocked or the SSH target is only reachable through jump-host or SSH-config-only routing. Stdio mode keeps all traffic inside SSH and does not require inbound UDP, but it may not notice sleep or Wi-Fi loss promptly. See `docs/remote-resilience.md` for concise operational notes. | |||
| 153 | - | ||||
| 154 | - | ## Terminal color | |||
| 155 | - | ||||
| 156 | - | When the attaching client reports truecolor support, vev advertises newly spawned panes as `TERM=xterm-direct` and exports `COLORTERM=truecolor` to child processes. Otherwise, panes use the conservative `TERM=xterm-256color` and do not receive `COLORTERM`. This works for local and remote attach because the client capability is carried inside vev's protocol rather than relying on SSH `SendEnv`/`AcceptEnv`. | |||
| 157 | - | ||||
| 158 | - | Applications that use terminfo can detect direct color from `xterm-direct`; applications that use the common environment convention can use `COLORTERM=truecolor` when it is present. Panes restored from a saved snapshot start conservatively before any client attaches; after attach or resume, newly spawned panes use that client's reported capability. | |||
| 159 | - | ||||
| 160 | - | ## Terminal compatibility | |||
| 161 | - | ||||
| 162 | - | vev keeps a server-side VT screen for each pane. It tracks scroll regions with DECOM origin mode, ANSI insert mode, cursor-position and mode-query reports, alternate-screen state, bracketed paste, mouse tracking, synchronized updates, OSC 9/777 notifications, OSC 52 clipboard set requests, and double-width cells. Resize and edit operations repair double-width cells so CJK or emoji glyph halves are not left behind. | |||
| 163 | 37 | ||||
| 164 | ## Keys | 38 | ## Keys | ||
| 165 | 39 | ||||
| 166 | - | All bindings use Alt directly; there is no prefix key. | 40 | + | No prefix key; everything is Alt. |
| 167 | - | ||||
| 168 | - | | Key | Action | | |||
| 169 | - | |---|---| | |||
| 170 | - | | Alt+Space | open the command palette | | |||
| 171 | - | | Alt+f | show or hide the current tab's floating terminal | | |||
| 172 | - | | Alt+1 .. Alt+9 | switch to tab by number | | |||
| 173 | - | | Alt+h/j/k/l | focus pane left/down/up/right | | |||
| 174 | - | | Alt+Arrow | focus pane by direction | | |||
| 175 | - | | Alt+a | jump to a session with attention | | |||
| 176 | - | ||||
| 177 | - | Some terminals encode Alt+Space as a non-breaking space instead of `ESC Space`; in those terminals, the palette binding depends on whether the terminal can send `ESC Space` for Alt+Space. | |||
| 178 | - | ||||
| 179 | - | ## Command palette | |||
| 180 | - | ||||
| 181 | - | Open the palette with Alt+Space. Type a command code or fuzzy-match the label/description. Use Up/Down or Ctrl-N/Ctrl-P to move, Tab to complete the selected command code, Enter to run, and Esc/Ctrl-C to close. With an empty query, Tab completes the most recently successful command; commands that take an argument complete with a trailing space and preserve an argument already entered. | |||
| 182 | - | ||||
| 183 | - | Successful commands are promoted near the top the next time the palette opens. `BSK` toggles back to your previously active session. Typing `JRS` lists recent sessions by number in the status bar, and `JRS N` jumps to number N; the numbers are captured when you start typing, so a busy MRU list never shifts under you. An invalid or stale number leaves the palette open. | |||
| 184 | - | ||||
| 185 | - | | Code | Action | | |||
| 186 | - | |---|---| | |||
| 187 | - | | CNT | create new tab | | |||
| 188 | - | | CNS | create and switch to a named session | | |||
| 189 | - | | CLT | close current tab | | |||
| 190 | - | | SPR / SPL / SPU / SPD | split pane right / left / up / down | | |||
| 191 | - | | STP | stack a new pane | | |||
| 192 | - | | TST | toggle focused stack | | |||
| 193 | - | | CLP | close focused pane | | |||
| 194 | - | | FPL / FPR / FPU / FPD | focus pane left / right / up / down | | |||
| 195 | - | | NXT / PVT | switch to next / previous tab | | |||
| 196 | - | | BSK | toggle the previously active session | | |||
| 197 | - | | JRS N | jump directly to recent session rank N | | |||
| 198 | - | | SSP | open session picker | | |||
| 199 | - | | VIS | enter visual mode | | |||
| 200 | - | | FLT | show or hide the current tab's floating terminal | | |||
| 201 | - | | RNS | rename session | | |||
| 202 | - | | RNT | rename current tab | | |||
| 203 | - | | DET | detach | | |||
| 204 | - | ||||
| 205 | - | Rename prompts are prefilled with the current session name. Empty names and names already used by another session are rejected. | |||
| 206 | - | ||||
| 207 | - | ## Session picker | |||
| 208 | - | ||||
| 209 | - | Open it with `SSP` in the palette. It lists sessions and tabs — tab entries use the same `name (title)` form as the top bar, ellipsized to the list width — previews the selected target when space allows, and marks stopped sessions. Wide pickers place the preview beside the list with a muted divider; narrower pickers stack it below the list, then hide it when neither layout has useful space. A clipped preview stays anchored to the bottom of the target's visible terminal frame and never loads its scrollback. | |||
| 210 | 41 | ||||
| 211 | | Key | Action | | 42 | | Key | Action | | ||
| 212 | |---|---| | 43 | |---|---| | ||
| 213 | - | | j / k, Up / Down | move selection | | 44 | + | | Alt+Space | command palette | |
| 214 | - | | Enter | switch to or resume selected target | | 45 | + | | Alt+f | floating terminal for the current tab | |
| 215 | - | | x | kill/delete selected target | | 46 | + | | Alt+1 .. 9 | switch tab | |
| 216 | - | | q, Esc, Ctrl-C | close picker | | 47 | + | | Alt+h/j/k/l, Alt+Arrow | focus pane | |
| 217 | - | 48 | + | | Alt+a | jump to a session needing attention | | |
| 218 | - | ## Panes and tabs | |||
| 219 | 49 | ||||
| 220 | - | Tabs hold pane layouts. Panes can be split left/right/up/down, stacked, focused by direction, and closed from the command palette. Clicking a pane focuses it. | 50 | + | The palette does the rest: type a short code (`SPR` split right, `CNT` new tab, `SSP` session picker, ...) or fuzzy-search the command list. Scroll up with the mouse to enter scrollback; vim keys move, `v` selects, `y` copies via OSC 52. |
| 221 | 51 | ||||
| 222 | - | Each tab also owns one independent floating terminal. It prewarms asynchronously on the tab's first visit, opens centered at the configured percentage of the pane-content area, and dims normal pane content while leaving status bars and popup chrome crisp. Hiding it retains the process, screen, and scrollback; switching tabs restores each tab's own visible or hidden state. Process exit clears the slot so the next open recreates it. The title combines the foreground process and OSC terminal title as `ProcessName: ProcessTerminalTitle`, falling back to either available title and then the configured command or shell. | 52 | + | ## Remote attach |
| 223 | 53 | ||||
| 224 | - | While visible, the floating terminal receives ordinary input, bracketed paste, mouse input, clipboard-path injection, and scrollback/copy mode. Global vev bindings remain active, and a bare Esc is passed to the floating application. `Alt+f` and `FLT` toggle visibility; there is no separate floating-pane close command. | 54 | + | ```sh |
| 55 | + | vev attach user@host[:session] | |||
| 56 | + | ``` | |||
| 225 | 57 | ||||
| 226 | - | The top bar shows each tab as `name (title)`, where the name is the tab's rename or number and the title is the focused pane's `ProcessName: ProcessTerminalTitle`. On truecolor clients the name renders bold and the parenthesized title slightly muted (in both the top bar and the session picker), derived from the client's reported theme; other terminals render plain labels. Set `tabs.terminal-title = off` to drop the `ProcessTerminalTitle` half from tab labels (top bar and session picker) and show only the process name; floating and stacked pane title bars are unaffected. Labels share the row width: when everything fits they render in full, otherwise unused space flows from short tabs to long ones and long labels are ellipsized, degrading to the bare tab name when space runs out. The bottom bar shows the active session followed by recent sessions, fading toward older entries. Ephemeral sessions are marked with `*`, for example `0*`. | 58 | + | vev must be installed on the remote. If your firewall only allows SSH, open the UDP range first (default `61000:61023`, override with `VEV_UDP_PORT_RANGE`): |
| 227 | 59 | ||||
| 228 | - | ## Scrollback mode and copy | 60 | + | ```sh |
| 61 | + | sudo ufw allow 61000:61023/udp | |||
| 62 | + | ``` | |||
| 229 | 63 | ||||
| 230 | - | Scrollback mode freezes a view over history while the program keeps running underneath. Enter it with the `VIS` palette command or by scrolling up with the mouse wheel. Passive browsing is labeled `[SCROLL]`; selecting lines changes the status to `[SELECT]`. Scrolling back to the bottom exits to the live pane. | 64 | + | Where UDP is not an option, `VEV_REMOTE_TRANSPORT=stdio` keeps everything inside SSH at the cost of slower disconnect detection. Details in [docs/remote-resilience.md](docs/remote-resilience.md). |
| 231 | 65 | ||||
| 232 | - | | Key | Action | | 66 | + | ## Configuration |
| 233 | - | |---|---| | |||
| 234 | - | | j / k, Up / Down | move one line | | |||
| 235 | - | | PgUp / PgDn | move one page | | |||
| 236 | - | | g / G | jump to top / bottom | | |||
| 237 | - | | v or Space | toggle line selection | | |||
| 238 | - | | y or Enter | copy selection and exit | | |||
| 239 | - | | q, Esc, Ctrl-C | exit without copying | | |||
| 240 | 67 | ||||
| 241 | - | Copy uses OSC 52 clipboard sequences when supported by your terminal. After a successful copy, the bottom bar briefly shows `copied N chars to clipboard`. | 68 | + | Optional file at `~/.config/vev/config`, reloaded live. Themes, key bindings, palette codes, the floating terminal, status bar scripts: see [docs/configuration.md](docs/configuration.md). Terminal color and VT compatibility notes live in [docs/terminal.md](docs/terminal.md). |
| 242 | 69 | ||||
| 243 | ## Development | 70 | ## Development | ||
| 244 | 71 | ||||
| 245 | ```sh | 72 | ```sh | ||
| 246 | - | go install golang.org/x/tools/cmd/goimports@latest | |||
| 247 | - | go install github.com/vektra/mockery/v2@latest | |||
| 248 | make test # go test ./... -race | 73 | make test # go test ./... -race | ||
| 249 | - | make lint # goimports check, then go vet ./... | 74 | + | make lint # goimports check, go vet |
| 250 | make mocks # regenerate mocks | 75 | make mocks # regenerate mocks | ||
| 251 | ``` | 76 | ``` | ||
A docs/configuration.md +85 -0
| @@ -0,0 +1,85 @@ | |||
| 1 | + | # Configuration | |
| 2 | + | ||
| 3 | + | vev reads `~/.config/vev/config` (`$XDG_CONFIG_HOME` respected). No file means defaults. The daemon picks up changes within a couple of seconds; no restart needed. | |
| 4 | + | ||
| 5 | + | ```text | |
| 6 | + | # Theme: auto follows the client; dark/light force built-in palettes. | |
| 7 | + | theme = auto | |
| 8 | + | ||
| 9 | + | # Palette placement: auto, center, top-left, top, top-right, left, right, | |
| 10 | + | # bottom-left, bottom, or bottom-right. | |
| 11 | + | palette.anchor = auto | |
| 12 | + | ||
| 13 | + | # One prewarmed floating terminal per tab. An empty command uses the normal shell. | |
| 14 | + | floating.command = | |
| 15 | + | floating.width = 80% | |
| 16 | + | floating.height = 80% | |
| 17 | + | ||
| 18 | + | # Show the focused pane's terminal title in tab labels; off keeps the process name only. | |
| 19 | + | tabs.terminal-title = on | |
| 20 | + | ||
| 21 | + | # Rebindable actions. Leave a line out to keep its built-in binding. | |
| 22 | + | open-palette = alt+space | |
| 23 | + | toggle-floating-pane = alt+f | |
| 24 | + | jump-attention = alt+a | |
| 25 | + | focus-pane-left = alt+h | |
| 26 | + | focus-pane-right = alt+l | |
| 27 | + | focus-pane-up = alt+k | |
| 28 | + | focus-pane-down = alt+j | |
| 29 | + | switch-tab-1 = alt+1 | |
| 30 | + | # ... through switch-tab-9 = alt+9 | |
| 31 | + | ||
| 32 | + | # Processes relaunched when a named session is restored. Empty disables relaunch. | |
| 33 | + | snapshot.restore_processes = vi,vim,nvim,emacs,man,less,more,tail,top,htop,btop,claude,codex,pi,opencode | |
| 34 | + | ||
| 35 | + | # Right bar anchors: commands run on the daemon host. Empty disables an anchor. | |
| 36 | + | bar.top-right = vev-bar-top-right | |
| 37 | + | bar.bottom-right = vev-bar-bottom-right | |
| 38 | + | bar.interval = 5s | |
| 39 | + | ||
| 40 | + | # Command palette codes: 2-3 letters or digits. | |
| 41 | + | code.new-tab = CNT | |
| 42 | + | code.new-session = CNS | |
| 43 | + | code.close-tab = CLT | |
| 44 | + | code.split-right = SPR | |
| 45 | + | code.split-left = SPL | |
| 46 | + | code.split-up = SPU | |
| 47 | + | code.split-down = SPD | |
| 48 | + | code.stack-pane = STP | |
| 49 | + | code.toggle-stack = TST | |
| 50 | + | code.close-pane = CLP | |
| 51 | + | code.focus-pane-left = FPL | |
| 52 | + | code.focus-pane-right = FPR | |
| 53 | + | code.focus-pane-up = FPU | |
| 54 | + | code.focus-pane-down = FPD | |
| 55 | + | code.next-tab = NXT | |
| 56 | + | code.previous-tab = PVT | |
| 57 | + | code.back-session = BSK | |
| 58 | + | code.jump-recent-session = JRS | |
| 59 | + | code.session-picker = SSP | |
| 60 | + | code.visual-mode = VIS | |
| 61 | + | code.toggle-floating-pane = FLT | |
| 62 | + | code.rename-session = RNS | |
| 63 | + | code.rename-tab = RNT | |
| 64 | + | code.detach = DET | |
| 65 | + | ``` | |
| 66 | + | ||
| 67 | + | Invalid values log a warning and keep the last valid value. | |
| 68 | + | ||
| 69 | + | ## Bindings | |
| 70 | + | ||
| 71 | + | Key specs: `alt+<char>`, `alt+space`, `alt+left/right/up/down`, `alt+1` through `alt+9`. Configuring an action replaces all of its built-in aliases (set `focus-pane-left` and the Alt+Arrow alias is gone). Tab switching also accepts the top-row symbols of non-QWERTY layouts, so AZERTY works without extra config. | |
| 72 | + | ||
| 73 | + | ## Palette anchor | |
| 74 | + | ||
| 75 | + | `auto` uses a bottom shelf on narrow terminals and a bottom-right rail from 96 columns up. Reload moves an open palette without losing your query. | |
| 76 | + | ||
| 77 | + | ## Floating terminal | |
| 78 | + | ||
| 79 | + | The command runs through your shell. A changed command applies on the next launch; changed dimensions on the next show or resize. Floating state is not restored across daemon restarts. | |
| 80 | + | ||
| 81 | + | ## Bar anchors | |
| 82 | + | ||
| 83 | + | Anchor commands run on the daemon host every `bar.interval` (minimum 1s). vev reads the first line of stdout, strips ANSI codes, and keeps the last good value on failure. Scripts get `VEV_ANCHOR`, `VEV_SESSION`, `VEV_TAB`, `VEV_PANE`, `VEV_PANE_CWD`, and `VEV_COLS`. | |
| 84 | + | ||
| 85 | + | The default bottom-right script runs `git status --porcelain` on each refresh; raise the interval or replace it if that is too heavy for your repository. | |
| @@ -0,0 +1,85 @@ | |||||
| 1 | + | # Configuration | |||
| 2 | + | ||||
| 3 | + | vev reads `~/.config/vev/config` (`$XDG_CONFIG_HOME` respected). No file means defaults. The daemon picks up changes within a couple of seconds; no restart needed. | |||
| 4 | + | ||||
| 5 | + | ```text | |||
| 6 | + | # Theme: auto follows the client; dark/light force built-in palettes. | |||
| 7 | + | theme = auto | |||
| 8 | + | ||||
| 9 | + | # Palette placement: auto, center, top-left, top, top-right, left, right, | |||
| 10 | + | # bottom-left, bottom, or bottom-right. | |||
| 11 | + | palette.anchor = auto | |||
| 12 | + | ||||
| 13 | + | # One prewarmed floating terminal per tab. An empty command uses the normal shell. | |||
| 14 | + | floating.command = | |||
| 15 | + | floating.width = 80% | |||
| 16 | + | floating.height = 80% | |||
| 17 | + | ||||
| 18 | + | # Show the focused pane's terminal title in tab labels; off keeps the process name only. | |||
| 19 | + | tabs.terminal-title = on | |||
| 20 | + | ||||
| 21 | + | # Rebindable actions. Leave a line out to keep its built-in binding. | |||
| 22 | + | open-palette = alt+space | |||
| 23 | + | toggle-floating-pane = alt+f | |||
| 24 | + | jump-attention = alt+a | |||
| 25 | + | focus-pane-left = alt+h | |||
| 26 | + | focus-pane-right = alt+l | |||
| 27 | + | focus-pane-up = alt+k | |||
| 28 | + | focus-pane-down = alt+j | |||
| 29 | + | switch-tab-1 = alt+1 | |||
| 30 | + | # ... through switch-tab-9 = alt+9 | |||
| 31 | + | ||||
| 32 | + | # Processes relaunched when a named session is restored. Empty disables relaunch. | |||
| 33 | + | snapshot.restore_processes = vi,vim,nvim,emacs,man,less,more,tail,top,htop,btop,claude,codex,pi,opencode | |||
| 34 | + | ||||
| 35 | + | # Right bar anchors: commands run on the daemon host. Empty disables an anchor. | |||
| 36 | + | bar.top-right = vev-bar-top-right | |||
| 37 | + | bar.bottom-right = vev-bar-bottom-right | |||
| 38 | + | bar.interval = 5s | |||
| 39 | + | ||||
| 40 | + | # Command palette codes: 2-3 letters or digits. | |||
| 41 | + | code.new-tab = CNT | |||
| 42 | + | code.new-session = CNS | |||
| 43 | + | code.close-tab = CLT | |||
| 44 | + | code.split-right = SPR | |||
| 45 | + | code.split-left = SPL | |||
| 46 | + | code.split-up = SPU | |||
| 47 | + | code.split-down = SPD | |||
| 48 | + | code.stack-pane = STP | |||
| 49 | + | code.toggle-stack = TST | |||
| 50 | + | code.close-pane = CLP | |||
| 51 | + | code.focus-pane-left = FPL | |||
| 52 | + | code.focus-pane-right = FPR | |||
| 53 | + | code.focus-pane-up = FPU | |||
| 54 | + | code.focus-pane-down = FPD | |||
| 55 | + | code.next-tab = NXT | |||
| 56 | + | code.previous-tab = PVT | |||
| 57 | + | code.back-session = BSK | |||
| 58 | + | code.jump-recent-session = JRS | |||
| 59 | + | code.session-picker = SSP | |||
| 60 | + | code.visual-mode = VIS | |||
| 61 | + | code.toggle-floating-pane = FLT | |||
| 62 | + | code.rename-session = RNS | |||
| 63 | + | code.rename-tab = RNT | |||
| 64 | + | code.detach = DET | |||
| 65 | + | ``` | |||
| 66 | + | ||||
| 67 | + | Invalid values log a warning and keep the last valid value. | |||
| 68 | + | ||||
| 69 | + | ## Bindings | |||
| 70 | + | ||||
| 71 | + | Key specs: `alt+<char>`, `alt+space`, `alt+left/right/up/down`, `alt+1` through `alt+9`. Configuring an action replaces all of its built-in aliases (set `focus-pane-left` and the Alt+Arrow alias is gone). Tab switching also accepts the top-row symbols of non-QWERTY layouts, so AZERTY works without extra config. | |||
| 72 | + | ||||
| 73 | + | ## Palette anchor | |||
| 74 | + | ||||
| 75 | + | `auto` uses a bottom shelf on narrow terminals and a bottom-right rail from 96 columns up. Reload moves an open palette without losing your query. | |||
| 76 | + | ||||
| 77 | + | ## Floating terminal | |||
| 78 | + | ||||
| 79 | + | The command runs through your shell. A changed command applies on the next launch; changed dimensions on the next show or resize. Floating state is not restored across daemon restarts. | |||
| 80 | + | ||||
| 81 | + | ## Bar anchors | |||
| 82 | + | ||||
| 83 | + | Anchor commands run on the daemon host every `bar.interval` (minimum 1s). vev reads the first line of stdout, strips ANSI codes, and keeps the last good value on failure. Scripts get `VEV_ANCHOR`, `VEV_SESSION`, `VEV_TAB`, `VEV_PANE`, `VEV_PANE_CWD`, and `VEV_COLS`. | |||
| 84 | + | ||||
| 85 | + | The default bottom-right script runs `git status --porcelain` on each refresh; raise the interval or replace it if that is too heavy for your repository. | |||
M docs/remote-resilience.md +2 -2
| @@ -5,7 +5,7 @@ Remote attach starts with SSH bootstrap and then carries the vev session over au | |||
| 5 | 5 | ## Operational model | |
| 6 | 6 | ||
| 7 | 7 | - SSH bootstrap starts or finds the remote UDP endpoint. | |
| 8 | - | - UDP carries normal session traffic after bootstrap. | |
| 8 | + | - UDP carries normal session traffic after bootstrap. The remote proxy binds a port in a fixed range, `61000-61023` by default; `VEV_UDP_PORT_RANGE` overrides it (a range, a single port, or `0` for a random ephemeral port). | |
| 9 | 9 | - Named sessions remain recoverable across daemon restarts; ephemeral sessions remain recoverable only while the daemon retains them. | |
| 10 | 10 | - `VEV_REMOTE_TRANSPORT=stdio` keeps all traffic inside SSH when direct UDP is blocked. | |
| 11 | 11 | ||
| @@ -19,7 +19,7 @@ Resilience work uses these user-facing states: | |||
| 19 | 19 | | `degraded` | authenticated UDP packets still arrive, but complete-record or ACK progress is delayed | | |
| 20 | 20 | | `probing` | authenticated UDP packet contact is absent, so vev probes for path recovery | | |
| 21 | 21 | | `resuming` | a replacement transport is attaching to the same session | | |
| 22 | - | | `offline` | authenticated UDP contact remains absent and vev resumes over SSH stdio | | |
| 22 | + | | `offline` | authenticated UDP contact remains absent; vev re-runs the SSH bootstrap and re-dials UDP with the resume token | | |
| 23 | 23 | | `expired` | the session or recovery window is no longer available | | |
| 24 | 24 | ||
| 25 | 25 | When a terminal is in raw mode, recovery messages appear as vev status text rather than as bytes written into the remote program's output stream. | |
| @@ -5,7 +5,7 @@ Remote attach starts with SSH bootstrap and then carries the vev session over au | |||||
| 5 | ## Operational model | 5 | ## Operational model | ||
| 6 | 6 | ||||
| 7 | - SSH bootstrap starts or finds the remote UDP endpoint. | 7 | - SSH bootstrap starts or finds the remote UDP endpoint. | ||
| 8 | - | - UDP carries normal session traffic after bootstrap. | 8 | + | - UDP carries normal session traffic after bootstrap. The remote proxy binds a port in a fixed range, `61000-61023` by default; `VEV_UDP_PORT_RANGE` overrides it (a range, a single port, or `0` for a random ephemeral port). |
| 9 | - Named sessions remain recoverable across daemon restarts; ephemeral sessions remain recoverable only while the daemon retains them. | 9 | - Named sessions remain recoverable across daemon restarts; ephemeral sessions remain recoverable only while the daemon retains them. | ||
| 10 | - `VEV_REMOTE_TRANSPORT=stdio` keeps all traffic inside SSH when direct UDP is blocked. | 10 | - `VEV_REMOTE_TRANSPORT=stdio` keeps all traffic inside SSH when direct UDP is blocked. | ||
| 11 | 11 | ||||
| @@ -19,7 +19,7 @@ Resilience work uses these user-facing states: | |||||
| 19 | | `degraded` | authenticated UDP packets still arrive, but complete-record or ACK progress is delayed | | 19 | | `degraded` | authenticated UDP packets still arrive, but complete-record or ACK progress is delayed | | ||
| 20 | | `probing` | authenticated UDP packet contact is absent, so vev probes for path recovery | | 20 | | `probing` | authenticated UDP packet contact is absent, so vev probes for path recovery | | ||
| 21 | | `resuming` | a replacement transport is attaching to the same session | | 21 | | `resuming` | a replacement transport is attaching to the same session | | ||
| 22 | - | | `offline` | authenticated UDP contact remains absent and vev resumes over SSH stdio | | 22 | + | | `offline` | authenticated UDP contact remains absent; vev re-runs the SSH bootstrap and re-dials UDP with the resume token | |
| 23 | | `expired` | the session or recovery window is no longer available | | 23 | | `expired` | the session or recovery window is no longer available | | ||
| 24 | 24 | ||||
| 25 | When a terminal is in raw mode, recovery messages appear as vev status text rather than as bytes written into the remote program's output stream. | 25 | When a terminal is in raw mode, recovery messages appear as vev status text rather than as bytes written into the remote program's output stream. | ||
A docs/terminal.md +13 -0
| @@ -0,0 +1,13 @@ | |||
| 1 | + | # Terminal support | |
| 2 | + | ||
| 3 | + | ## Color | |
| 4 | + | ||
| 5 | + | When the client reports truecolor, new panes get `TERM=xterm-direct` and `COLORTERM=truecolor`; otherwise `TERM=xterm-256color`. The capability travels inside vev's own protocol, so it works over SSH without `SendEnv`/`AcceptEnv`. Panes restored from a snapshot start conservative until a client attaches. | |
| 6 | + | ||
| 7 | + | ## VT compatibility | |
| 8 | + | ||
| 9 | + | vev keeps a server-side VT screen per pane: scroll regions (DECOM), insert mode, cursor and mode-query reports, alternate screen, bracketed paste, mouse tracking, synchronized updates, OSC 9/777 notifications, OSC 52 clipboard, and double-width cells. Resizes repair double-width cells so CJK and emoji halves are never left behind. | |
| 10 | + | ||
| 11 | + | ## Alt+Space caveat | |
| 12 | + | ||
| 13 | + | Some terminals encode Alt+Space as a non-breaking space instead of `ESC Space`. If the palette does not open, rebind `open-palette`. | |
| @@ -0,0 +1,13 @@ | |||||
| 1 | + | # Terminal support | |||
| 2 | + | ||||
| 3 | + | ## Color | |||
| 4 | + | ||||
| 5 | + | When the client reports truecolor, new panes get `TERM=xterm-direct` and `COLORTERM=truecolor`; otherwise `TERM=xterm-256color`. The capability travels inside vev's own protocol, so it works over SSH without `SendEnv`/`AcceptEnv`. Panes restored from a snapshot start conservative until a client attaches. | |||
| 6 | + | ||||
| 7 | + | ## VT compatibility | |||
| 8 | + | ||||
| 9 | + | vev keeps a server-side VT screen per pane: scroll regions (DECOM), insert mode, cursor and mode-query reports, alternate screen, bracketed paste, mouse tracking, synchronized updates, OSC 9/777 notifications, OSC 52 clipboard, and double-width cells. Resizes repair double-width cells so CJK and emoji halves are never left behind. | |||
| 10 | + | ||||
| 11 | + | ## Alt+Space caveat | |||
| 12 | + | ||||
| 13 | + | Some terminals encode Alt+Space as a non-breaking space instead of `ESC Space`. If the palette does not open, rebind `open-palette`. | |||