Skip to content

amarbel-llc/posh

 
 

Repository files navigation

POSH: the portable shell

posh makes a shell session portable: it roams across networks and persists across disconnects, so the session you start follows you — survive a sleep, change networks, detach here and reattach from another machine, and the shell and its running processes are exactly where you left them.

It delivers this as a single Rust tool that combines two lineages:

  • zmx (terminal session persistence — attach/detach from sessions without killing the underlying processes, window management delegated to the OS window manager), and
  • mosh (roaming remote terminal over encrypted UDP that survives sleep, network changes, and intermittent connectivity).

Portability is the two together: mosh-style roaming carries a session across the network, zmx-style persistence carries it across time and machines, and the unified host:session namespace addresses both with one grammar.

This repository hosts the rewrite as a Cargo workspace. The original C++ mosh tree is kept under zz-mosh/ as the porting reference (with its own justfile for the host-lane build: just zz-mosh/<recipe>); the original zmx (Zig) lives in its own repository.

Layout

crates/
  posh-term/   standalone terminal emulation library (no dependencies)
  posh/        the posh binary
  posh-proto/  shared frame/display protocol: Snapshot + new_frame renderer, frame codecs (DumpDiff/MorphDelta), ServerFrame/FrameBody wire types, RFC 0001 caps (github #75)
  poshterity/    deterministic terminal recorder/replayer (lib + poshterity bin; posh rec)
  mosh-ffi/    C++ FFI oracle: drives mosh's terminal + predictor for differential tests (dev/test only; ADR 0004)
doc/           scdoc man-page sources (man posh, posh-server, posh-client, poshterity, posh(7))
docs/          ADRs, RFCs, feature records (FDRs), plans, and the manual test plan
posht/         interactive terminal-capability test (Go; nix build .#posht)
zz-mosh/       the C++ mosh reference tree (buildable: nix build .#mosh)

crates/posh-term

A from-scratch Rust rewrite of the ghostty-vt terminal core as a completely independent, dependency-free library crate. It is pure state: feed it PTY bytes with Terminal::process, query the resulting screen, and drain query replies (DA/DSR/OSC/kitty) with take_responses.

Feature set targets kitty parity, with the sequences fish shell integration relies on fully supported:

  • Williams DEC parser state machine; incremental UTF-8 with malformed-input replacement; C1 controls; colon SGR subparams.
  • Primary screen with scrollback ring + alternate screen; wide chars, combining marks, pending-wrap semantics, BCE, margin-aware scrolling, origin mode, tab stops, DECALN, REP.
  • Full SGR including styled underlines (single/double/curly/dotted/dashed), underline color (58/59), 256-color and truecolor in both ; and : forms.
  • Modes: DECCKM, DECOM, DECAWM, DECTCEM, alt screen 47/1047/1049, bracketed paste 2004, mouse 9/1000/1002/1003 with SGR/SGR-pixel protocols, focus reporting 1004, synchronized output 2026, IRM, LNM.
  • OSC: 0/1/2 title, 4/10/11/12 palette and dynamic colors (set + query), 7 (cwd), 8 (hyperlinks), 52 (clipboard), 133 (shell-integration prompt marks, as emitted by fish), 9/99 (notifications), 22 (pointer shape).
  • Kitty keyboard protocol: full flag stack (push/pop/set/query) plus a client-side encode_key covering legacy and CSI u encodings.
  • Kitty graphics protocol: APC G parsing, transmit/place/delete/query, RGB/RGBA/PNG formats, chunked transmission, 320 MB quota, spec ACKs.
  • DCS: DECRQSS, XTGETTCAP; queries: DA1/DA2, DSR, DECRQM, XTVERSION, XTWINOPS 14/16/18.
  • Serialization: dump_text() (plain text including scrollback) and dump_vt() (an escape stream that reconstructs contents, attributes, cursor, modes, title, scroll region, and kitty graphics — images, placements, animation frames — on a real terminal, verified by roundtrip tests). This is what powers session replay on attach and remote state sync.

Also implemented: reflow on resize (logical lines rewrap via wrap flags, wide-char aware, scrollback included; alt screen truncates/pads like kitty), DECCOLM/DECNCSM column switching, kitty graphics relative placements and animation frame storage with the full delete-specifier set, file-based graphics transmission, OSC 52 per-selection slots, the xterm color stack (XTPUSHCOLORS/XTPOPCOLORS/XTREPORTCOLORS), DECSTR soft reset, selective erase (DECSCA/DECSED/DECSEL), and a client-side encode_mouse covering X10/normal/UTF-8/SGR/SGR-pixel.

Graphics payloads are fully decoded in-crate with no dependencies: a hand-written RFC 1950/1951 inflate (for o=z) and a PNG decoder (8-bit gray/gray+alpha/RGB/RGBA/indexed, all filters, PLTE/tRNS, CRC-verified) feed f=100 images and animation frames as RGBA, with frame compositing (a=c, blend or replace) and composed_frame() for renderers.

Known simplifications: Adam7-interlaced and 16-bit PNGs are rejected; shared-memory graphics transmission answers EUNSUPPORTED (sandbox-safe); OSC 66 text sizing is parsed but scale is not rendered.

crates/posh

The combined CLI. No async runtime; poll()-based event loops like both originals.

Session persistence (zmx port):

posh attach <name> [command...]    # or bare: posh <name>; detach: Ctrl-\
posh list [--short|--json]
posh run <name> [--] <command...>
posh fork [<name>]                 # fork current session (same cmd + cwd)
posh detach [<name>] | detach-all
posh kill <name>
posh groups
posh history <name> [--vt]
posh completions <bash|zsh|fish>

Attaching takes over the outer terminal's alternate screen (terminfo smcup/rmcup for $TERM via a built-in term(5) reader, hardcoded 1049 when no database answers, skipped under --no-init/$POSH_NO_TERM_INIT or for terminals without an alternate screen) and detaching restores it, so the shell prompt you attached from comes back exactly as you left it (FDR 0002: docs/features/). The daemon virtualizes the session's own alt-screen switches and RIS in the broadcast — replaced with model-generated repaints — so full-screen apps inside the session can never flip the outer terminal off posh's screen. Session scrollback is reached by scrolling the wheel at a bare prompt (FDR 0005) — or as text via posh history; the outer terminal's native scrollback stays the shell's own.

Daemon-per-session over Unix sockets with zmx's binary IPC framing (1-byte tag + u32 LE length; Input/Output/Resize/Detach/DetachAll/Kill/Info/Init/ History/Run/Ack/Exit — Exit carries the shell's status so an attached client exits with the session's real code). Each daemon feeds PTY output through a posh_term::Terminal so re-attaching clients receive a full state replay via dump_vt_flat(). Session groups via -g/--group or POSH_GROUP; socket directory resolution: POSH_DIR > XDG_RUNTIME_DIR/posh > TMPDIR/posh-{uid} > /tmp/posh-{uid}. Sessions export POSH_SESSION/POSH_GROUP.

Remote roaming (mosh port) and the unified namespace:

posh [user@]host [-- command]      # like mosh(1): plain roaming shell
posh [user@]host:[group/]session   # persistent session on the host over
                                   # the roaming transport — attach-or-
                                   # create, detach here, reattach from
                                   # anywhere; [fe80::1]:dev for IPv6
posh host:[group/]session --detach # create a detached session on the host
                                   # and return (remote attach --detach)
posh :[group/]session              # explicit local attach
posh -g GROUP list host:           # remote listing (host-prefixed names);
                                   # -g scopes it to GROUP on the host
posh ssh [-4|-6] [-p RANGE] [user@]host [-- command]
posh server [new] [-p PORT[:PORT2]] [-4|-6] [-- command...]
posh client [-4|-6] <host> <port>  # key via POSH_KEY, never on argv

The grammar is scp-style and total (RFC 0001: docs/rfcs/); every pre-namespace form keeps its meaning. The remote session's exit status propagates: posh box:dev; echo $? reports the session shell's code.

Tailscale peers are first-class hosts: shell completion offers tailnet peer names (MagicDNS) alongside ~/.ssh/config aliases and session names (posh tailnet lists them), and the roaming transport falls back to a peer's tailnet IP when the system resolver can't reach its MagicDNS name. Both degrade silently without tailscale.

The ssh bootstrap runs posh-server new on the remote host (mosh-server parity); the package installs posh-server as an alias of posh, so the server host just needs the package on its non-interactive PATH.

Encrypted UDP datagrams using AES-128-GCM with mosh's nonce layout (direction bit + 63-bit sequence number), replay protection with a reorder window, timestamp echo for RFC 6298 RTT estimation, fragmentation for large frames, and server-side roaming by adopting the source address of the newest authenticated datagram (late reorders never re-target the stream). State sync sends complete dump_vt() frames (or a prefix/suffix diff against the last acked frame); a client that advertises SCROLLBACK also accumulates the primary-screen scrollback incrementally (append-only rows, per-frame cost bounded by inter-frame growth rather than ring depth — RFC 0002); user input is delivered reliably via cumulative offsets and retransmission.

The client takes over the alternate screen for the whole connection (mosh's smcup/rmcup) and restores the pre-connect shell screen on exit and around suspend. It renders mosh-style: it maintains a local posh_term::Terminal, morphs the real tty with minimal per-cell diffs (a port of terminaldisplay.cc), and runs a faithful port of mosh's prediction engine (terminaloverlay.cc): speculative local echo with epochs, confirmation against server echo-acks, adaptive display with mosh's SRTT/glitch/flagging constants, and underlined predictions when the link is slow (POSH_PREDICTION_MODEL: always/never/adaptive/experimental). POSH_PREDICTION_MODEL=optimistic instead echoes keystrokes immediately and lets the next paint correct them, gated on the remote PTY's ECHO flag and alt-screen (FDR 0006). The prediction model and the render style are independent, swappable axes: POSH_PREDICTION_RENDER selects how predicted cells are painted (replace, the default, or dim). POSH_PREDICTION is a deprecated alias for POSH_PREDICTION_MODEL. A reverse-video "Last contact N seconds ago" banner appears after 6.5s of silence. Ctrl-^ opens a command palette — the escape menu — listing the predictive-echo models, the debug-logging toggle, escape-to-shell on the server, suspend, and quit (rendered by the posh-palette helper); Ctrl-^ then . is a fallback quit if that helper can't launch. Servers bind dual-stack IPv6 when possible, report POSH IP from $SSH_CONNECTION for the ssh wrapper, require UTF-8 locales on both ends (forwarding LANG/LC_* over ssh), forward TERM and COLORTERM so the session isn't left color-blind (TERM resolved to an entry the remote's terminfo database actually has), and honor POSH_SERVER_NETWORK_TMOUT / POSH_SERVER_SIGNAL_TMOUT.

The renderer also ports mosh's scroll optimization (matched rows are scrolled with \r\n runs or a DECSTBM region instead of being rewritten) and emits OSC 8 hyperlinks.

Because the whole connection lives on the outer terminal's alternate screen, the mouse wheel at a bare prompt is at the mercy of that terminal's alt-screen wheel behavior — kitty, for one, turns it into arrow keys and ignores DECSET 1007 (posh#3/#28). posh grabs the wheel (mouse reporting) at a bare prompt and, by default, scrolls the session's own primary-screen scrollback locally from the client's accumulated ring — instant, no round-trip, frozen while scrolled, with a top status-bar indicator; any keystroke or scrolling back to the bottom resumes the live view (FDR 0005, posh#43). POSH_GRAB_MOUSE=on reverts the wheel to the legacy transform (wheel up/down → arrow keys, normalizing scroll across terminals, posh#50) instead of scrollback. Either way, grabbing the wheel costs the outer terminal's click-to-select while active; a session app that tracks the mouse itself (vim, tmux) keeps the wheel.

Known simplifications relative to mosh: frames carry dump_vt() state (or a prefix/suffix diff) rather than mosh's SSP protobuf instructions with zlib; no utmp/motd integration. The full parity contract — what is mirrored, what is deliberately dropped, and the open gaps — is FDR 0003 (docs/features/), with the living checklist in issue #44.

crates/poshterity

A deterministic terminal recorder/replayer built on posh-term: replay a recorded output byte stream through the in-process emulator and inspect the exact screen, with no live terminal and no timing to race (the tmux capture-pane + sleep flake that motivates it). Depends only on posh-term; surfaced as the standalone poshterity binary and as posh rec.

poshterity record [--out f.castx] -- <cmd>       # record a command under a PTY
posh --record f.castx <session>                # record a live posh session
poshterity replay <file> [--dump text|vt|flat]   # or: posh rec replay ...
poshterity step <file> --by change --n 3         # step-debug, dump each screen
poshterity bless  <file> --golden g --at K       # write a golden-frame snapshot
poshterity assert <file> --golden g --at K       # check it (CI gate)

The recording format is .castx, a strict superset of asciinema .cast v2 (standard o/i/r events plus an ignorable m marker and a poshterity header block), so any .cast replays through poshterity and any .castx plays in asciinema. step advances by an emulator-defined granularity (byte/escape/write/change/frame/marker) and dumps the intermediate screen — a deterministic VT100 frame debugger. bless/assert snapshot the screen at a marker (grid is diff-friendly text + a style sidecar; vt is the raw escape stream) — the deterministic analog of tmux capture-pane; a library of typed assertion helpers (find_line, cells_have_fg/bg, cells_are_*) renders a colored expected-vs-actual diff on mismatch. Issue #56 tracks the epic; adoption + the .castx RFC land in the final phase.

Building and testing

nix build                   # the full toolset: posh, posh-server, poshterity, posht (#73)
nix build .#posh            # hermetic build + cargo test (posh workspace; mosh-ffi gated separately)
just build-rust             # same, via the justfile lane
just debug-cargo test --workspace   # fast in-worktree dev-loop (includes mosh-ffi)
nix run .#poshterity -- ... # the recorder/replayer as a standalone tool
nix build .#checks.<sys>.mosh-ffi   # the C++ FFI oracle gate (just test-mosh-ffi)
nix build .#posht           # the interactive capability test (just build-go;
                            # part of `just build`/`test`). See docs/posht.md.

The workspace builds warning-free and carries ~400 tests (parser state machine, UTF-8 and wide-char edge cases, reflow, SGR colon forms, kitty keyboard encode vectors, graphics ACK paths, inflate and PNG decode vectors, frame compositing, dump_vt roundtrips, IPC framing, crypto seal/open/replay/tamper, fragmentation, RTT, prediction engine state transitions with injected clocks, display-diff and scroll-optimization morphing roundtrips, IPv6 loopback, and daemon lifecycle integration tests).

About

POSH: the portable shell — terminal sessions that roam across networks and persist across disconnects. Rust rewrite combining mosh (roaming remote terminal over encrypted UDP) and zmx (session persistence). — read-only mirror of code.linenisgreat.com/posh

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages

  • Rust 67.6%
  • C++ 17.2%
  • Go 3.3%
  • M4 3.3%
  • Just 2.5%
  • Shell 1.9%
  • Other 4.2%