A native, portable SSH client for Windows 11 and macOS written in Rust. A home
screen lists your saved connection targets; pick one (or start a new connection), fill
in host / port / user, pick an auth method (password, a private key — PEM or PuTTY
.ppk — keyboard-interactive for 2FA / OTP, or a key held by your SSH agent / Pageant),
connect. On success the server hands us a shell and cmote renders a full VT
terminal inside the window — a working interactive prompt, with a browsable tree of the
remote filesystem beside it, a grid of the current directory's files under it (keyboard
navigable, with a details popup and rubber-band multi-selection), the remote working
directory in the title bar, and file transfer both ways. Full-screen programs — btop,
vim, htop, midnight commander — draw properly and take the mouse. Reconnect to a saved
target and the shell and both panels come back to the directories you left them in. Open
as many sessions as you like in tabs — each fully independent, all in one window. Tunnel
ports through the connection — local, remote or a SOCKS proxy — and they come back on reconnect.
This is a learning project. The code is meant to be read as much as run, so it is
written didactically: it favours idiomatic Rust, explains why each choice was made,
and marks every deliberate shortcut with a ponytail: note so "simple" reads as
intent, not oversight. The full design rationale lives in PLAN.md; section
references below (§n) point into it.
- Tabs — many independent sessions in one window. Each tab is its own session: one can sit on the home list while another runs a shell, and every tab keeps its own terminal, folder tree, files pane, selection and dialogs. A background tab's shell keeps running and its listings keep arriving while you work in another. Mouse-only strip across the top: click a tab to switch, "+" to open a new one, "×" to close (a live session asks to confirm first). The saved targets and the unlocked vault are shared across every tab.
- Confirmed, clean quit. Closing the last tab, or clicking the window's title-bar ×, asks Quit cmote? first — telling you how many live sessions it will disconnect — so a stray click never drops your work. On confirm, every session is disconnected cleanly (a proper SSH channel close, not a yanked socket) and only then does the process exit; a wedged session can't hold quit open past a short timeout. Ctrl+D closes the current tab, but only once you're back on the home screen — on a live shell it stays EOF to the remote (the way you log out), so Ctrl+D logs out, and Ctrl+D again closes the tab, just like a terminal (§30).
- Port forwarding — local, remote and dynamic tunnels. The Tunnels button on the status
bar opens a manager: add a Local (
-L) forward to reach a service through the server, a Remote (-R) forward to expose a local service on the server, or a Dynamic (-D) SOCKS5 proxy that lets each connection pick its own target. Each tunnel rides the same connection — no second login — and shows live / failed in the dialog; the set is remembered per target and re-established when you reconnect. Binds to loopback by default. - Home screen of saved targets — every successful connection is remembered as a
named target and listed alphabetically. Profiles only by default: no passwords or
passphrases in
targets.json(only host / port / user / auth method / key path) — a secret is stored only if you opt in to the encrypted vault (below). Click a target to select it, then click it again (or press Enter) to open it and pre-fill the form; rename it in place with F2 or right-click → Rename (the list re-sorts); right-click also offers Open and Delete (deleting asks to confirm — cancelling keeps the target); New connection opens a blank form. - Connection form: host, port, user, and an auth method.
- Password auth, or private-key auth with a native file picker (
rfd). - Key formats: OpenSSH / PEM (via
russh::keys) and PuTTY.ppk(viassh-key'sfrom_ppk). Encrypted keys prompt for a passphrase on their own screen — or pre-fill an optional passphrase field on the form (leave it empty to be prompted). - Keyboard-interactive (2FA / OTP) — pick Interactive for challenge-response servers, and cmote also chains into it automatically after a password/key when the server asks for a second factor (key/password plus a one-time code). The server's prompts appear one field each — masked for a code or password, plain for a username — and are answered live; nothing is stored.
- SSH agent / Pageant — pick Agent to let a running agent hold the key and sign the
challenge, so cmote never sees the private key and there is nothing to type. On Windows it
looks for the OpenSSH agent (the
\\.\pipe\openssh-ssh-agentpipe, orSSH_AUTH_SOCKwhen it points at one) and then Pageant; on macOS it usesssh-agentviaSSH_AUTH_SOCK. Every agent key is offered in turn until the server accepts one, and it still chains into 2FA afterwards. - Remember a secret (opt-in, portable) — tick Remember on the form to keep that
target's password or key passphrase in an encrypted vault (
secrets.age), so a return visit pre-fills the masked field. The vault is one file protected by a master passphrase you choose (age: scrypt + XChaCha20-Poly1305), so it stays portable — it unlocks on any machine with the passphrase, unlike a machine-bound OS keyring. Off by default; the secret is saved only after a successful connect (a wrong password is never stored), and a forgotten master passphrase means the secrets are gone (no recovery, by design). - Trust-on-first-use host-key verification against a portable
known_hosts: first contact shows the fingerprint for explicit accept/reject; a later key change opens a loud override dialog — both fingerprints (stored vs presented), a possible-MITM warning, and reject / trust-once / replace, defaulting to reject and never auto-trusting (§8, §28). - A full VT terminal — a complete VT engine (
alacritty_terminal) whose grid cmote draws with iced — that reflows to the window size, forwarding the new pty size to the remote (§9, §23). - Full-screen programs draw properly — btop, htop, vim, midnight commander. The screen
is one widget that puts every glyph at the exact pixel its column starts at, so nothing a
program prints can shift the line it is on; braille graphs and rounded box corners
— glyphs no monospace font we could bundle actually carries — are drawn from their own
geometry rather than borrowed from whatever font the system offers. The engine interprets
the full escape-sequence set — the DEC line-drawing characters older programs box-draw
with, custom tab stops, origin mode — so a program's screen lands where it belongs instead
of coming out as wrapped, scrolling gibberish. It also answers the queries a program
blocks on or adapts to — "where is the cursor?" (
CSI 6n), "what terminal are you?" (CSI c), "what is your background colour?" (OSC 11, which lets an editor pick a light or dark colourscheme to match), "how big is your screen?" — that otherwise stall vim, tmux and less on a startup timeout or leave them guessing; cmote answers each with what it actually shows (§9, §23). F1-F12 are mapped as the pty's terminfo entry describes them, and a modifier held on a named key now goes through: Ctrl+arrow for word-motion, Shift+arrow to select by line, Ctrl+Delete, modified F-keys, and F13-F24 all send the sequence xterm would, where before the modifier was dropped (§9). And when an editor asks for modifyOtherKeys, the Ctrl/Alt combos the plain terminal alphabet cannot spell — Ctrl+digit, Ctrl+punctuation, Ctrl+C as a distinct key rather than the interrupt — reach it unambiguously (§9). Editors that speak the newer kitty keyboard protocol (neovim, kakoune, helix, fish) get the fuller treatment: Esc told apart from an Alt combo, every key disambiguated, and — for the ones that ask — press / repeat / release events and the key's associated text, all in kitty'sCSI uform (§25). - Text styling comes through — colour (256-colour and truecolor), bold, faint, reverse video, concealed text, strikethrough, and every underline style a program reaches for: single, double, dotted, dashed and the curly one an editor draws under a spelling mistake, each in its own colour when the program sets one, plus italic — drawn from a bundled IBM Plex Mono face, since Fira Mono ships no italic of its own (§23).
- The cursor takes the shape a program asks for — a block, an underline, or a thin bar, whichever the remote picks with DECSCUSR (vim's insert-mode bar, say); drawn steady, since cmote runs no blink timer (§23).
- The remote is told when focus changes — a program that turns on focus reporting (
?1004, as tmux and vim do) hearsCSI I/CSI Oas the window gains or loses focus, so it can undim or pause a spinner. Moving cmote's keyboard to a side panel counts as the shell losing focus too, since the remote knows nothing of cmote's own panels (§23). - Scroll back over what left the screen — cmote keeps 10 000 lines of history. The wheel scrolls it (whenever no full-screen program has claimed the wheel), Shift+PageUp/PageDown page through it and Shift+Home/End jump to the top and back to the live bottom; typing (or pasting) snaps you back to the prompt so what you send lands where it echoes. New output while you are scrolled up leaves you where you are reading. A thin scroll indicator appears at the right edge while you are scrolled up and disappears at the live bottom — its bar shows where you are in the history and how deep it runs. A full-screen program (vim, tmux, less) keeps its own pages, so scrolling there is theirs, not cmote's (§23).
- Mouse text selection (drag to select, highlighted in place) with Copy and Paste — from the status-bar buttons, a right-click menu, or the keyboard. Ctrl+C copies (when a selection exists; otherwise it is the shell's interrupt) as styled HTML that keeps the terminal's colours and attributes when pasted into a rich editor, with a plain-text fallback; Ctrl+Shift+C copies plain text only. Ctrl+V / Ctrl+Shift+V paste, bracketed-paste aware and stripping the paste-injection terminator (§9-§10).
- Clickable links — a program that marks text as a hyperlink (the OSC 8 escape, as
ls --hyperlinkand many build tools do) makes it followable: Ctrl+click opens it in your browser, or right-click for Open link / Copy link. cmote opens only http, https and mailto — a link's scheme decides which local program Windows launches, and the address comes from the remote, so anything else is refused (§24). - The mouse reaches the program that asked for it — click a process in btop, a tab in tmux, a line in vim; the wheel scrolls what is under it. cmote forwards clicks, releases, drags and scrolls in the xterm protocols a program enables, and holding Shift takes the pointer back for text selection and cmote's own right-click menu (§9).
- Remote folder tree — a 2D explorer of the remote filesystem in the bottom strip, to
the right of the files pane (the terminal keeps the full width above), over SFTP
(falling back to
lson a server with the subsystem disabled). Click a folder to expand or collapse it; the tree follows the shell, opening the whole chain from/down to wherever youcd. Right-click a folder for Open in terminal (types a quotedcd), New folder…, Upload… (sends local files into that folder), Upload folder… (sends a whole local folder), Rename… (inline, like F2 on the home list), Delete…, Copy name / relative path / full path and Refresh (re-checks the folder is still there, under its name, and re-lists what is inside). A single folder opens and closes by clicking its row or with → / ←, so there is no menu Expand/Collapse. Its header names the folder on show — middle-ellipsised and capped at two lines — with a copy button, a ↻ refresh button and a collapse-all button beside it. The refresh button (and F5 while the tree has focus) re-lists every open folder at once, so the tree catches up in one press after you move or make a folder from the shell; collapse-all closes every branch back to the top level. Drag the splitter to resize the panel — the terminal reflows to match — or hide it with the status bar's Folders button; the.*checkbox in its header hides dot-folders (§18, §22). - Remote files pane — a grid of every entry in one directory, in the browser strip
under the terminal (the folder tree shares that strip, on the pane's right, §18). Each cell
is a wide row: a small icon in front of the name, with the size and
the modified date, and the
owner:groupon a second, muted line underneath (4.0 KB · 2026-03-20 11:46 · cme:staff, the date in the server's own wall clock but without the zone tag — that stays in the details popup; a folder shows only the date and the owner, and anything the listing never learned reads as a dash). Each cell carries a thin border so the grid reads as distinct tiles. A name too long for its cell is middle-ellipsised, so the start and the extension survive. A big directory streams in batches of 1000 and the header counts as they land. Icons come from a bundled icon font, by category (folder, image, code, archive, document, audio, video, link, plain). Right-click an entry for Open in terminal, Download…, Download folder… (a lone directory, tree and all), Rename…, Delete…, Copy name / relative path / full path and Refresh; right-click empty space for New folder…, Upload… here, Upload folder… here and Refresh. The header carries an up button, the directory's path (middle-ellipsised to one line), a copy button for it and a ↻ refresh button (re-lists the directory on show; F5 does the same while the pane has focus). A sort button beside it drops a menu to order the grid by Name, Last modified, Extension or Size, Ascending or Descending — folders always stay grouped first. The order and the direction are each optional: picking the lit key clears the sort back to the default order, picking the lit direction unsets it (an unset direction sorts ascending), and the button lights up while a key is reordering the grid. The chosen sort is remembered per target and restored on reconnect. Drag the splitter to resize the pane, or hide it with the status bar's Files button; the same.*checkbox hides dot-files here too (§19). - Browsing never moves the console — a click in the folder tree, a double-click on a
folder in the grid, the pane's up button and Enter all point the pane somewhere
else and leave the shell where it is, so you can look inside a directory without disturbing
what is running. The shell moves only on a
cdit can see: one you type, either panel's Open in terminal, or the status bar's Sync, which brings the shell (and with it the tree and the title) to the folder the pane is showing. Sync is disabled when the two already agree (§19). - Keyboard focus across the three regions — the shell, the folder tree and the files pane each take the keyboard. A session starts at the shell; a click focuses whatever was clicked, Ctrl+Tab cycles forward and Ctrl+Shift+Tab back (hidden panels are skipped), and the focused panel wears a ring so it is never a guess. In a panel the arrow keys walk the rows (in the grid, left/right move one cell and up/down a whole row), Tab / Shift+Tab step next/previous, Enter opens, F2 renames and Esc hands the keyboard back to the shell. A keyboard-moved selection scrolls itself into view, only at the edges (§20).
- A details popup beside the selection — the entry's full name (the grid's label is
narrow and may clip it), where a symlink points, the file's MIME type, its
modification time in the server's own timezone (
2026-03-20 11:46:40 CEST (+02:00)— the zone is read off the server once per session), its size (human, with the exact byte count behind it) and itsowner:groupas names, not numbers. Anything the server would not say reads as a dash, and a button on the card copies the whole thing at once (§20, §22). - Selecting many entries at once — drag a rubber band over the grid's empty space, Ctrl+click to add or remove one, Shift+click or Shift+arrow to take the run between two ends, Ctrl+A to take the lot; Ctrl+drag adds a band to what is already selected. The popup then summarises the set (how many, folders versus files, total size). A right-click inside the selection acts on all of it — the copy items join their results one per line and say how many they will take — while a right-click outside collapses onto that one entry first (§21).
- File download — right-click a file in the pane → Download…, pick where to save
it in the native dialog, and it comes down over SFTP on its own channel with a
progress bar in the status bar. Downloading a multiple selection asks for one
destination folder instead of a dialog per file, queues the transfers (one at a time, one
progress bar) and leaves any folders in the selection behind. If some of those names are
already in the folder, one dialog asks about the whole batch before anything is written:
Skip them, Save alongside (
notes-1.txt), Replace, or Cancel (§19, §21). - File upload, one or many, into a folder — pick local files with Files… (the
picker is multi-select) and send them with Upload, over SFTP on its own channel so
the shell keeps running. The confirmation lists what you picked under an editable
destination folder — each file keeps its own name inside it, and an empty folder means
the login directory. Upload starts from four places, each seeding that folder: the status
bar (the shell's directory), the terminal's right-click Upload… (the shell's
directory), the files pane's empty-space Upload… here (the pane's directory), and a
folder's Upload… in the tree. Before a byte is sent, every destination name is checked
on the server; if some are already there, one dialog asks about the whole batch —
Replace, Skip, Keep both (
name-1.txt) or Cancel. The files then go one at a time behind the status bar's progress bar, closing withUploaded N files, and a failure names its reason and moves on to the next (§17). When the batch lands, the files pane (and the tree) re-list the destination folder if they are showing it, so what you just sent appears in place without a manual Refresh (§29). - Create and delete remote entries — New folder… (on the tree and the pane's
empty-space menus) opens a small name dialog; Delete… (on either menu, and on a whole
files-pane selection) removes what you picked over SFTP — a folder goes with its entire
subtree — behind a confirmation that names the targets and warns it cannot be undone. Both
fall back to
mkdir/rm -rfon a server with the sftp subsystem disabled (§18). - Recursive folder transfer — Upload folder… sends a whole local directory tree onto the
server, and Download folder… pulls a whole remote one down, each recreating the tree on the
other side and merging into a destination that already exists. When a file inside the tree
would land on one already there, cmote asks one file at a time: Overwrite, Keep both
(
name-1), or Skip just this one; Overwrite all or Skip all to settle every later clash the same way; or Cancel the whole transfer (files already copied stay). Symlinks are skipped, never followed, so a cyclic link can't loop it (§17, §19). - Drag a file in to upload it — drag a file off the desktop and drop it anywhere on the window; it uploads into the files pane's current directory, reusing the same pre-scan and, on a name already there, the same Overwrite / Keep both / Skip / Cancel dialog the menu upload uses. While a file is dragged over the window the pane wears a green ring to say where the drop will land. One file at a time this iteration — a folder or a second file is declined with a note (use Upload folder… for a whole tree). Dragging a remote file out onto the desktop is not offered: the GUI toolkit can receive an OS drop but cannot start one, so pulling files down stays the right-click Download… (§29).
- The remote working directory in the window title — cmote reads the
OSC 7/OSC 9;9sequences shells emit on each prompt, so the title followscdon POSIX and Windows remotes. bash and zsh are hooked up automatically when the shell opens; fish and Windows Terminal-style prompts already announce it themselves (§17). When a program sets its own title (OSC 0/OSC 2—vimnaming the file it is editing, say), that shows in the title bar instead; the host is always kept alongside so the window stays identifiable (§23). - Consistent dialogs — the delete-target, disconnect, upload and overwrite
confirmations, the host-key prompt, the passphrase prompt, and the error notice share
one chrome: a header bar (question on the left, close ✕
on the right, wired to the safe action), an explanatory body, and evenly-spaced footer
buttons. Each floats over the page it belongs to (the connect-flow dialogs over the
connect form, the disconnect modal over the shell) behind a dim backdrop; clicking the
card never dismisses it (only a click outside does); the body message is selectable and
copyable — drag to select,
Ctrl+Cto copy (handy for the host-key fingerprint or an error message); and the dialog is draggable by its header, clamped to the window (§10). - Every copy says so — any Copy (a menu item, a header's copy button, the details card's) raises a short toast at the bottom of the window that fades itself after three seconds, so a copy is never a silent no-op you have to test by pasting (§10).
- Resuming where you left off — a saved target remembers, per target, the shell's
directory, the files pane's directory, the
.*toggle, the files-pane sort (key and direction) and both panel sizes. On the next connection the pane reopens there, the tree reveals the chain down to it, and the shell is put back with a visiblecd. The snapshot is written at every teardown — a clean Disconnect, a remote hangup, an error — and a value this session never learned never erases the one already saved. Profile metadata only: still no secrets on disk (§22). - Secrets — passwords and key passphrases are held in memory and
zeroized on drop, and by default never written to disk (§12); only non-secret connection profiles are persisted, for the home list (§14). The one exception is the opt-in encrypted vault (§16): tick "Remember" and the secret is kept insecrets.age, sealed with a master passphrase you choose — portable across machines, and off unless you ask for it.
Everything the mouse and the keyboard do, by region. The focused region is the one that gets a keystroke; a click focuses what it lands on, and the ring shows where the keyboard is.
Anywhere in the window
| Gesture | What it does |
|---|---|
| Ctrl+Tab / Ctrl+Shift+Tab | Move the keyboard to the next / previous region — shell, folder tree, files pane (hidden panels are skipped) |
| Click a region | Focus it |
| Ctrl+D (home screen only) | Close the current tab; closing the last one asks to quit cmote. On a live shell it stays EOF to the remote instead |
| Window title-bar × | Ask Quit cmote?, then disconnect every session cleanly and exit |
| Drag a dialog's header | Move the dialog; Esc or ✕ takes the dialog's safe way out |
| Drag inside a dialog's body | Select its text; Ctrl+C copies it |
Terminal (the shell)
| Gesture | What it does |
|---|---|
| Drag across the grid | Select text (highlighted in place) |
| Right-click | Context menu: Copy / Paste / Upload… (into the shell's directory); on a link cell, Open link / Copy link too |
| Ctrl+click a link | Open an OSC 8 hyperlink in the browser (http/https/mailto only) |
| Ctrl+C | Copy the selection as styled HTML + plain text (rich paste keeps colours); with no selection, the shell's interrupt instead. Clears the selection after copying |
| Ctrl+Shift+C | Copy the selection as plain text only |
| Ctrl+V / Ctrl+Shift+V | Paste (bracketed-paste aware); both paste plain text |
| Copy / Paste via the status-bar buttons or right-click menu | Same copy (rich) and paste |
| Click / drag / scroll in a program that asked for the mouse | Goes to that program (btop, vim, tmux, mc) instead of selecting |
| Shift + click or drag | Takes the pointer back: select text, or right-click for cmote's own menu |
| Any other key | Goes to the remote shell — arrows (SS3 form in application-cursor mode), F1-F12, modified named keys (Ctrl/Shift/Alt + arrows / Home / End / F-keys, F13-F24 included), modifyOtherKeys Ctrl/Alt combos (CSI 27;…~), and the kitty keyboard protocol (CSI u, incl. key-release events) when an editor turns either mode on |
| Drag either splitter | Resize the folder tree or the files pane; the pty is reflowed to match |
| Sync in the status bar | cd the shell to the folder the pane is showing (disabled when they already agree) |
| Files… / Upload in the status bar | Pick local files, then send them into the shell's directory |
Folder tree (right of the files pane, in the bottom strip — the status bar's Folders button hides it; shown only alongside the files pane)
| Gesture | What it does |
|---|---|
| Click a folder | Expand or collapse it, and select it |
| Right-click a folder | Open in terminal / Upload… / Rename… / Copy name / Copy relative path / Copy full path / Refresh |
| ↑ / ↓, Tab / Shift+Tab | Walk the visible rows |
| → / ← | Open / close the selected folder |
| Enter | cd the shell into it |
| F2 | Rename in place (Enter commits, Esc abandons) |
| F5 / header ↻ button | Refresh — re-list every open folder in one press |
| Header collapse-all button | Close every branch back to the top level |
| Esc | Give the keyboard back to the shell |
| Copy button in the header | Copy the path of the folder on show |
.* checkbox |
Hide or show dot-entries (shared with the files pane) |
Files pane (under everything — the status bar's Files button hides it)
| Gesture | What it does |
|---|---|
| Click an entry | Select it (and show its details popup) |
| Double-click a folder | Show it in the pane; the shell stays where it is |
| Click empty space | Clear the selection |
| Drag from empty space | Rubber-band selection; Ctrl+drag adds to what is selected |
| Ctrl+click | Add or remove one entry |
| Shift+click | Select everything between the anchor and here |
| Ctrl+A | Select every entry on show |
| Right-click an entry | The entry's menu — on a multiple selection it acts on all of it |
| Right-click empty space | Upload… here / Refresh |
| Drag a file from the desktop onto the window | Upload it into the folder on show (one file); the pane rings green while it hovers |
| Copy button in the details popup | Copy the whole details card |
| ← / → | Move one cell; ↑ / ↓ move a whole row |
| Shift + those arrows | Extend the selection instead of moving it |
| Tab / Shift+Tab | Next / previous entry |
| Enter | Show the selected folder in the pane |
| F2 | Rename in place |
| F5 / header ↻ button | Refresh — re-list the directory on show |
| Sort button in the header | Menu: order by Name / Last modified / Extension / Size, Ascending or Descending; folders stay first. Pick the lit key to clear the sort, the lit direction to unset it (unset sorts ascending). Remembered per target |
| Esc | Give the keyboard back to the shell |
| ↑ button in the header | Show the parent directory |
| Copy button in the header | Copy the path of the directory on show |
Home screen
| Gesture | What it does |
|---|---|
| Click a target | Select it; click again (or Enter) to open it |
| Right-click a target | Open / Rename / Delete (deleting asks first) |
| F2 | Rename the selected target (Enter commits, Esc abandons) |
| Delete | Delete the selected target, after the confirmation (Esc cancels it) |
| Tab / Shift+Tab on the connect form | Move focus across the fields, the auth radios and Connect; Enter / Space activates the focused radio or button |
- Rust stable (developed against 1.91.0 on Windows, 1.97.1 on macOS).
- Windows 11 — target
x86_64-pc-windows-msvcand the MSVC toolchain (Visual Studio Build Tools with the VC++ x64 tools and the Windows SDK — the default MSVC linker). No NASM or C compiler: theringcrypto backend ships pre-generated assembly for this target (§2). - macOS Sequoia (Intel) — target
x86_64-apple-darwinand the Xcode Command Line Tools (clang), which compilering's crypto from source. No NASM (§2). - No external SSH library on either target — the SSH stack is pure Rust (§12).
# Debug build and run
cargo run
# Optimized, self-contained portable binary
cargo build --release
# Windows → target/release/cmote.exe
# macOS → target/release/cmoteOn Windows the release cmote.exe is portable: copy it anywhere (including a USB
stick) and run it — no installer, no registry writes, no external runtime.
On macOS wrap the binary in a minimal app bundle so Finder launches it as a GUI app (double-clicking a bare Unix binary would open a Terminal window instead):
cargo build --release
./bundle-macos.sh # → target/release/cmote.app
open target/release/cmote.appcmote.app is self-contained and relocatable — no installer or external runtime. It
is not code-signed or notarized yet (deferred — §12), so the first launch needs a
right-click → Open to clear Gatekeeper's "unidentified developer" prompt.
cmote writes up to three files — known_hosts (pinned host keys), targets.json (saved
connection profiles plus where each session left off: the two directories, the .* toggle
and the panel sizes — no secrets), and, only once you opt in to remembering a secret,
secrets.age (the encrypted credential vault, §16 — a master-passphrase-sealed age blob,
the sole place any secret is stored). All live in the same directory, resolved at
runtime (§11, paths::data_dir):
- Portable mode (preferred):
cmote-data/beside the binary, when that directory is writable. This keeps the data travelling with the app — on macOS the binary lives incmote.app/Contents/MacOS/, so the store sits inside the bundle. - Fallback (Windows):
%LOCALAPPDATA%\cmote\when the exe sits in a read-only location (e.g.Program Files); on macOS~/Library/Application Support/cmote/.
To reset trust for a host, delete the offending line (or the whole file) from
known_hosts. To drop a saved target, use right-click → Delete in the app and confirm
the prompt (or delete its entry from targets.json) — deleting a target also forgets its
vault secret when the vault is unlocked. To forget every remembered secret at once, delete
secrets.age; a forgotten master passphrase is unrecoverable, so this is also the only way
back in if you lose it (you keep the profiles, just re-enter the secrets).
Pure logic is unit-tested; anything needing a live server is manual (§13). No test
framework is pulled in — everything uses Rust's built-in #[test] / #[cfg(test)].
cargo test # run the unit tests
cargo fmt # format (rustfmt, hard tabs — see rustfmt.toml)
cargo clippy --all-targets -- -D warningsCI (.github/workflows/ci.yml) runs these same gates on every push and pull
request to main, on both targets — cargo fmt --check plus cargo clippy -D warnings and cargo test on Windows (x86_64-pc-windows-msvc) and macOS
(clippy against the Intel target x86_64-apple-darwin, tests native on the runner).
It also audits the dependency tree: cargo audit for RustSec advisories and
cargo deny (see deny.toml) for the license allow-list, banned crates (no
aws-lc-* — keeps the NASM-free portable build, §12), and trusted sources.
Automated coverage: key parsing (the .ppk header sniff, Ed25519 .ppk loaded plain and
encrypted, the encrypted-key passphrase re-ask paths, and a non-.ppk blob routed to the
OpenSSH loader — the .ppk fixtures are Ed25519, the format from_ppk also reads RSA / ECDSA /
DSA §7), host-key match/unknown/mismatch decisions and
fingerprint formatting, terminal byte-stream → grid, key-event → byte-sequence
mapping (including application-cursor-mode arrow keys, CSI vs SS3, every F1-F12
against the terminfo entry, the modified named keys — Ctrl/Shift/Alt + arrows /
navigation / F-keys and F13-F24 — and modifyOtherKeys, both the stream scanner that
detects the mode and the CSI 27;mod;code~ encoding it switches on, and the kitty keyboard
protocol per flag — disambiguate, event types incl. release, report-all, associated text and
alternate keys — with the seam reading the pushed flags back and the engine answering the
CSI ? u query, §25), the terminal engine's
wiring end to end (an f-spelling move
lands in its own cell, a wide glyph reserves two columns, and the engine's query replies are
drained and sent back — device status, device attributes, a live cursor-position report, the
save/jump/report/restore size-probe reporting the clamped corner, and a query split across
two chunks answered on completion), pointer-event → mouse-report encoding (each encoding, each
mode's gating, the classic form's 223-column ceiling, the wheel, the modifier bits), the
grid's run packing and the geometry of the glyphs it draws itself (a braille
cell read back as its dot pattern, a rounded corner's arc and tails measured against a real
cell), the OSC 8 hyperlink surfaced on its cells and the link scheme allow-list (http/https/
mailto through, file: / vscode: / javascript: and a scheme-less URI refused, §24), the grid-resize
math, mouse-selection geometry and text extraction (wide
glyphs, trailing-blank trimming, multi-row joins), paste encoding (bracketed-paste
wrapping and the injection-terminator scrub), the remote-cwd scanner (OSC 7 and
OSC 9;9, split across chunks, percent-escapes, Windows paths, oversized payloads), and
the folder tree's model (row flattening and indentation, the hidden-folder filter,
subtree collapse, cd reveal and its no-op on a repeat, rename validation and the
post-rename refresh, relative-path arithmetic, shell quoting, and the panel's width
clamps), and the files pane's model (batch accumulation and the dropping of batches for a
directory already left, the cwd-follow rule that a repeated announcement is not a move,
the folders-first sort, icon categories from kind and extension, rename validation, and
the pane's height clamps). The keyboard and selection work adds: the arrow walk across both
panels (clamping at both ends, skipping hidden entries, and not panicking on an empty
directory), the keep-it-visible scroll rule (including an item taller than its viewport),
MIME types from extensions and their application/octet-stream fallback, mtime rendering in
a server timezone (the epoch, a leap day and both sides of Greenwich), the date +'%z %Z'
and ls -l longname parsers with their half-answer fallbacks, the link target belonging
to the selection that asked for it, the selection gestures (range from an anchor, toggle,
plain and additive band), the rubber band's hit-testing against the grid geometry (scrolled,
past the end of the listing, and in the gap between two rows), and — through the app's own
handlers rather than the model's — Shift+click and Shift+arrow, which is what proves the
modifier state reaches a mouse press. The upload, path-eliding and resume work adds: the
upload batch planner (every file queued under its own name when nothing clashes, and each
collision answer — Replace, Skip, Keep both — deciding what happens to each clashing file,
all without an App or a server), the middle-ellipsis cut (a short string left alone, a long
one keeping both ends inside its budget, and the cut never landing inside a glyph) and the
grid cell's two-line version of it, the short mtime that keeps the server's wall-clock shift
but drops the seconds and the zone tag, the session snapshot's round trip through
targets.json (including a pre-v2.2 file
with no session fields at all), and — again through the app's own handlers — a reconnect
that resumes both paths and pins the pane until the shell has caught up.
There is no CI SSH server in v1, so the end-to-end path is verified by hand against a
local sshd. Any reachable server works; the steps below use Docker for a disposable
one.
1. Start a throwaway server (creates user tester / password testpass on port
2222):
docker run --rm -d --name cmote-sshd -p 2222:22 \
-e USER_NAME=tester -e USER_PASSWORD=testpass -e PASSWORD_ACCESS=true \
linuxserver/openssh-server(Or use WSL / any host you control. On a native Windows OpenSSH server, connect to
localhost:22.)
2. Password auth + first-contact host key. Run cargo run, enter localhost,
port 2222, user tester, choose Password, type testpass, connect. Tab /
Shift+Tab should move focus across every control — the fields, the four auth radios, and
the Connect button (the active radio/button shows a highlight ring); Enter/Space
activates the focused radio or button. Expect:
- The Unknown host key dialog appears once, showing a SHA-256 fingerprint. You can
drag it by its header, select the fingerprint and copy it (
Ctrl+C), and closing (✕) rejects. Accept → the shell opens; the fingerprint is now pinned inknown_hosts. - Reconnecting no longer prompts (the key matches the pinned one).
3. Terminal behaviour. In the shell: run ls, echo hi, an interactive program
(top, then q), and Ctrl-C to interrupt. Print bold text
(printf '\033[1mBOLD\033[0m normal\n') and confirm the bold run is visibly heavier
than the normal one (both weights are bundled — §9). Print the other styles
(printf '\033[2mfaint\033[0m \033[3mitalic\033[0m \033[9mstruck\033[0m \033[4munder\033[0m \033[4:3mcurly\033[0m\n')
and confirm faint reads dimmer, italic slants (in IBM Plex Mono, §23), struck has a line
through it, and the two underlines differ — one straight, one wavy (§23). Ask the terminal its
background colour (printf '\033]11;?\033\\'): it replies on the input channel, so at a bash
prompt the answer rgb:1e1e/1e1e/1e1e appears as if typed — proof it reports what it draws (§23).
Change the cursor's shape (printf '\033[6 q' for a bar, \033[4 q an underline, \033[2 q
back to a block) and confirm it redraws each time — the shape a program like vim would pick (§23).
Turn on focus reporting (printf '\033[?1004h'), then click to another window and back: each
switch types a short ^[[O / ^[[I at the prompt, and tabbing the keyboard to the file tree
and back does the same — proof the remote is told (turn it off again with printf '\033[?1004l',
§23). Fill the screen with history (seq 1 200), then scroll back: the mouse wheel and
Shift+PageUp/PageDown move up through the run, Shift+Home jumps to the oldest line and
Shift+End back to the bottom, and typing any key snaps you back to the prompt (§23). As you
scroll up a thin bar appears at the right edge showing your place in the history — it is longer for
a shallow history and shorter for a deep one — and vanishes the instant you are back at the live
bottom. Scroll up,
then run something that prints (sleep 2; echo done in another split, or just wait for a clock) —
the view stays where you are reading rather than jumping to the new output. Open less /etc/services
or vim, scroll with the wheel, and confirm it pages the program (its own alternate screen has no
cmote scrollback), then quit and confirm the wheel scrolls cmote's history again (§23). Print wide glyphs over aligned
columns (e.g. printf '12\n世b\n') and confirm the character after a CJK/emoji glyph
stays in its column — a wide glyph reserves two cells (§9). Resize the window and run
tput cols; tput lines (or stty size) — the reported size should track the window.
With NumLock on, type a command using the numpad digits (e.g. echo 2 /
pm2 ls) and confirm the digits appear; with NumLock off, the numpad arrows
(2/4/6/8) should move the cursor instead of typing digits (§9). Click Disconnect
→ you return to the form immediately.
4. Key auth. Generate a test key and authorize it:
ssh-keygen -t ed25519 -f ./smoke_key -N "" # unencrypted
ssh-keygen -t ed25519 -f ./smoke_key_enc -N "hunter2" # encrypted
# copy the .pub of each into the server's ~tester/.ssh/authorized_keys- Unencrypted key: choose Key, browse to
smoke_key, connect → shell opens with no passphrase prompt. - Encrypted key: browse to
smoke_key_enc, connect → the Encrypted key screen appears with the field already focused; typehunter2→ shell opens. Enter a wrong passphrase first to confirm the prompt simply re-appears (bounded re-ask) before the correct one succeeds. - PuTTY
.ppk: convert a key with PuTTYgen and repeat — both encrypted and unencrypted.ppkshould behave like the OpenSSH cases. - SSH agent / Pageant: load
smoke_keyinto an agent (ssh-add ./smoke_key, or add it in Pageant on Windows), choose Agent — no fields appear — and connect → shell opens with no file to pick and no passphrase. Stop the agent (or empty it) and retry: expect a clear "no SSH agent found" / "no keys to offer" message, not a hang.
5. Host-key mismatch (override dialog). Delete the server container and start a fresh
one (new host key) on the same port, then reconnect. Expect the loud Host key has CHANGED
dialog: a red possible-MITM line and both SHA-256 fingerprints (stored vs presented,
selectable/copyable), with Reject / Trust once / Replace key. Closing (✕) or a
backdrop click rejects. Trust once connects without touching known_hosts (reconnect and it
warns again); Replace key pins the new key (reconnect and it is silent). A changed key is
never trusted without an explicit click (§8, §28).
6. Selection, copy, and paste. In the shell, run echo hello world, then drag
across the output to select it — the selection should highlight and Copy (status
bar or right-click menu) should enable. Copy, then Paste: the text lands at the
shell's cursor. Paste into a bracketed-paste-aware shell (bash/zsh with readline) and
confirm a multi-line clipboard does not auto-run each line (bracketed paste frames
it). Right-click anywhere to confirm the context menu opens at the cursor and dismisses
on a click away. Copy is disabled with nothing selected; pasting keeps the highlight.
7. Remote directory + upload. On connect, one setup line is echoed into the shell
(the cwd hook, §17) and the window title should read
cmote — tester@localhost:2222 — /config (or wherever the shell starts). cd /tmp and
the title should follow within a prompt. Set a title from a program
(printf '\033]2;my title\033\\') and the bar should switch to cmote — tester@localhost:2222 — my title; clearing it (printf '\033]2;\033\\') brings the directory back (§23). Then:
- Click Files…, pick a local file — its name appears next to the buttons and Upload
becomes enabled. Click Upload: the dialog lists the file under an editable destination
folder of
/tmp. Confirm → a progress bar with the byte count runs in the status bar, thenUploaded to /tmp/<name>, and the pick is cleared (Upload disabled again).ls -l /tmpon the remote should show it, byte-for-byte identical (sha256sumboth ends for a binary file). - Pick several files at once and upload them → they go one at a time, each with its own
progress bar, and the closing notice reads
Uploaded N files. - Upload the same batch again → the collision dialog names the files that are already
there: Cancel sends nothing (check the remote
mtimes), Skip leaves them alone, Keep both writesname-1.extbeside them, Replace overwrites. - Try the other three ways in — right-click the terminal → Upload… (destination is the shell's directory), right-click the files pane's empty space → Upload… here (the pane's directory, so point the pane elsewhere with the tree first and confirm the destination follows the pane, not the shell), and right-click a folder in the tree → Upload… (that folder).
- Drag a file in. Point the pane at a folder (a tree click, or
cdin the shell), then drag a file from the OS file manager over the window — the pane rings green — and drop it. It uploads into the pane's folder with no destination dialog, and the pane re-lists on its own so the new file appears in the grid without a manual Refresh (lsthere confirms it). Drop a file whose name is already there → the same Overwrite / Keep both / Skip / Cancel dialog. Drop a folder → a note declines it (single files only). Drop while a transfer is running → declined with the busy note. (Dragging a file out of the pane onto the desktop is not offered — use Download….) - Edit the destination in the dialog to a directory you cannot write (
/etc/x) → the status bar shows the failure and the shell stays open. - Start a shell that does not announce its directory (
docker exec … sh, or unset the hook withunset PROMPT_COMMAND; unset -f cmote_cwd) → the title drops the directory and the upload dialog offers an empty folder, which lands in the login directory. - Sort the grid. Point the pane at a mixed directory (
/usr/binis a good crowded one, or make one:mkdir /tmp/s && cd /tmp/s && mkdir b_dir a_dir && head -c 3000 /dev/zero > big.log && echo hi > small.txt && echo x > mid.md). Click the header's sort button → the menu drops below the toolbar, right beside the button, with four keys and — by default — neither direction ticked. Pick Name with no direction → the button lights up, folders stay grouped at the top, and each group runs A→Z (an unset direction sorts ascending). Pick Size, then Descending — the files reorder biggest-first; the menu stays open so you can set both halves without reopening it. Pick the lit Descending again → it unsets, back to ascending. Pick Extension → files group by their.log/.md/.txtending. Pick the lit key again → the sort clears, the button dims, and the grid returns to the default folders-first-by-name order. Click away → the menu closes. The chosen sort sticks across directories (browse elsewhere and back) until you clear it, and re-listing (F5) keeps it. - The sort is remembered per target. With a sort set (say Size, Descending),
Disconnect and reconnect to the same target → the pane reopens already sorted that way, the
button lit. Clearing the sort before disconnecting reopens in the default order. (A target you
never sorted, and a
targets.jsonfrom before this existed, reopen unsorted.)
8. Remote folder tree. The panel on the right should list / on connect. Then:
- Click folders to expand and collapse them; a slow directory shows
·until its listing arrives. Expand a few levels, collapse the top one, re-open it — it shows exactly one clean level again. Opening always re-lists, so a shell-side change under a collapsed folder is caught: with a folder open,mv one-of-its-children ../elsewhere/in the terminal, collapse that folder, then click it open again → the moved child is gone (the cached rows draw at once, then the fresh listing replaces them). cd /etc/sshin the shell → the tree opens/→/etc→/etc/sshon its own and highlights it.cdback and forth: the tree only ever expands, never closes what you opened.- Toggle the
.*checkbox in the panel header → dot-folders (.ssh,.config) disappear and reappear with no round trip. - Right-click a folder: Open in terminal should run a quoted
cdin the shell (make a folder with a space and a quote in its name —mkdir "/tmp/it's here"— and confirm thecdstill lands in it). Copy full path / Copy relative path / Copy name should put the right text on the clipboard (paste into the shell to check; the relative item is greyed out on a shell that never announces its directory). - Rename… turns the row into a field: Esc abandons, Enter commits and the row reappears sorted under its new name. Rename onto an existing name → the notice line under the tree says it already exists and nothing changed. Rename a folder you cannot write → the notice shows the refusal and the shell stays open.
- Refresh a change made from the shell. With a folder expanded, run
mkdir a/newin the terminal → the tree does not update on its own. Right-click the folder → Refresh re-lists it; the new child appears. Nowmv a a2in the shell and Refresh the (now stale)arow → it disappears anda2shows in its place: Refresh re-checks the folder's own name and existence (via its parent), not just its contents. Or press the header ↻ button (or F5 with the tree focused) → every open folder re-lists at once. A collapsed branch is left closed — re-opening it shows the fresh listing. - Collapse all. Expand several levels, then press the header collapse-all button → the tree snaps back to the root's own children, and re-opening any branch draws its cached rows instantly (then re-lists in the background to catch any change).
- Drag the splitter left and right: the grid reflows (
tput colsshould follow) and the panel stops at its minimum and at 60% of the window. The Folders button hides the panel and gives its columns back to the grid. - Against a server with the sftp subsystem disabled (
Subsystem sftpcommented out insshd_config), the tree should still list folders — thelsfallback (§18).
9. Remote files pane. The grid across the bottom should fill with / on connect,
then follow the shell. Then:
cd /etc→ the grid shows every entry in/etcwith an icon per type; the header names the directory and counts the entries. Create a directory with thousands of files (mkdir /tmp/many && cd /tmp/many && seq 1 5000 | xargs touch) and re-enter it: the count should climb in steps of 1000 as the batches land, and the window stays responsive throughout.- Each cell should read as a row: icon, name, and under it the size, the modified date
on the server's wall clock but with no zone tag, and the
owner:group(a folder shows only the date and the owner), each framed by a thin border so the grid reads as distinct tiles. Compare a few againstls -lon the remote — the clock, the size and the owner should match; the zone tag lives in the details popup. A very long name should be cut in the middle (report-fin…-draft.pdf), never at the extension. - Double-click a folder in the grid → the grid enters it and the shell stays put (the prompt's directory does not change, and the pane must NOT snap back on the next prompt). Same for the header's up button and Enter.
- Click a folder in the tree → the grid shows that folder while the shell stays where
it is, and it must NOT snap back on the next prompt. Click Sync in the status bar →
now the shell
cds there, the tree reveals it and the title follows; Sync greys out once the two agree. - Toggle the
.*checkbox → dot-files disappear from the grid and the tree together. - Right-click a file → Download… opens the save dialog; pick a path and the status bar runs a progress bar, then reports where it landed. Downloading onto an existing local file goes through the OS dialog's own replace prompt. Open in terminal is greyed out on a file, Download… on a folder.
- Rename… edits the label in place; Enter commits and the grid re-lists so the entry lands in its new sort position. Refresh (the menu item, the header ↻ button, or F5 with the pane focused) picks up a file created from the shell.
- Drag the horizontal splitter: the grid reflows (
tput linesshould follow) and stops at the minimum and at 60% of the window. The Files button hides the pane and gives its rows back to the terminal.
10. Keyboard focus and the details popup. Press Ctrl+Tab repeatedly: the focus ring should go shell → tree → files pane → shell. Hide one panel with its status-bar button and cycle again — the hidden stop is skipped. Then, in the files pane:
- Walk with the arrows: left/right move one cell, up/down a whole row, and both ends clamp instead of wrapping. Keep going past the bottom of the pane — the grid should scroll only when the selection reaches an edge, never re-centre. Tab / Shift+Tab step next and previous. Esc hands the keyboard back to the shell (type at the prompt to confirm).
- With an entry selected, the popup beside it should name the entry in full, and show the
MIME type (
text/x-pythonon a.py,application/octet-streamon something unknown), the time, the size andowner:group. Compare the time and the owner againstls -lon the remote — they should agree, including the timezone. Select a symlink (ln -s /etc /tmp/link-to-etc) → the popup adds→ /etca moment later. - Enter on a folder enters it; F2 renames in place.
11. Selecting many entries. In a directory with a dozen or so entries:
- Drag from empty space across several cells — a translucent rectangle follows the pointer and everything it touches highlights. Release outside the pane (over the terminal) and the band should end there, not keep selecting when the pointer comes back.
- Ctrl+drag a second band: it adds to the first selection instead of replacing it.
Ctrl+click toggles one entry, Shift+click takes the run between two, Shift+←/→/↑/↓
extends from the anchor, Ctrl+A takes them all. The popup should switch to
N items selectedwith the folders/files split and the total size. - Right-click inside the selection → the copy items carry the count; Copy full path should paste one path per line into the shell. Rename… and Open in terminal are greyed out. Right-click outside the selection → it collapses to that one entry first.
- Select several files (a folder among them is fine) → Download… (N) asks for a
destination folder, downloads them one at a time with the progress bar, skips the folder,
and finishes with
Saved N files. Run it again into the same folder → the Some of these files are already there dialog lists the names: Cancel downloads nothing, Skip them leaves the local copies untouched, Save alongside writesname-1.ext, Replace overwrites. Check the results withls -llocally.
12. Full-screen apps. Run vim (or less on a long file). The file should render, and
the arrow keys should move the cursor — this exercises application cursor mode (DECCKM):
the app enables it and cmote switches its arrow keys to the SS3 form so they register. In
vim, :q! to exit. Then the query answering and the harder cases:
-
Cursor-position probe. At the shell, run
printf '\033[6n'; read -rsdR r; echo "cursor: ${r#*[}". It should print the cursor's row;col at once and return to a prompt — if cmote did not answer,readwould hang until you press Enter. Then measure the screen:printf '\0337\033[999;999H\033[6n\0338'; read -rsdR r; echo "size: ${r#*[}"should report the terminal's actual rows;cols (resize the window and repeat — it should track). A program likevimortmuxshould now open without the ~1s startup pause its DA probe used to cost. -
Run btop (
brew install btopon a mac remote). Every panel should sit in its own box where it belongs — no line running on into the next, no frame drawn twice down the screen. btop positions its whole UI with cursor moves the previous engine could not follow; the VT engine (§23) interprets them, so the layout lands where it belongs. -
Its graphs should be dot patterns, evenly spaced inside their cells, and its box corners should be rounded and meet the straight lines cleanly. Both are drawn from geometry, not shaped from a font — no monospace font we could bundle has braille at all.
-
Press F2: btop's options menu should open. Esc closes it. (F1-F12 are mapped to the
xterm-256colorterminfo entry.) -
Click a process row — btop selects it. Scroll over the process list. Drag one of its sliders. Then hold Shift and drag across the screen: you should get cmote's own text selection instead, and Shift+right-click should open cmote's menu. Release Shift and the pointer belongs to btop again. Quit with
q. -
Run htop and mc (midnight commander) for a second opinion on both — mc lives on F1-F10 and is entirely mouse-driven.
-
Modified keys. At the shell (bash/zsh), hold Ctrl and press Left / Right: the cursor should jump a whole word, not one character — this is the modified named-key encoding (
ESC[1;5C), which was silently dropped before. Invim, Shift+arrow should extend a visual selection. (Bare Shift+PageUp/PageDown/Home/End still page cmote's own scrollback, §23 — that binding wins over the shell on purpose; the Ctrl/Alt variants reach the shell.) -
modifyOtherKeys. In neovim (or vim with
:set modifyOtherKeys=2), the Ctrl-combos a plain terminal cannot send now arrive: try mapping one, e.g.:nnoremap <C-,> :echo "got it"<CR>then press Ctrl+, — it should fire, where in a stock terminal it does nothing. Ctrl+letter bindings keep working too. Back at the bash prompt (mode off), Ctrl+C must still interrupt as always — the mode is the editor's to turn on, and off by default. -
Clickable links (OSC 8). At the shell, emit a link:
printf '\e]8;;https://example.com\e\\click me\e]8;;\e\\\n'. Ctrl+click the words click me — your browser should open example.com; right-click them for Open link / Copy link (Copy link should paste backhttps://example.com). Then emit a refused one —printf '\e]8;;file:///c:/windows\e\\nope\e]8;;\e\\\n'— and Ctrl+click it: nothing opens and a toast says the link was blocked (only http/https/mailto open, since the address is the remote's, §24). Plain text with no link is unaffected — Ctrl+click there just selects. -
Kitty keyboard protocol. In neovim on a recent build (it enables the protocol by default over a capable terminal) map a combo the legacy alphabet cannot spell, e.g.
:nnoremap <C-i> :echo "ctrl-i"<CR>— Ctrl+I should now fire without also triggering Tab, which a stock terminal cannot tell apart. Esc should feel instant (no Alt-combo wait). To see it end to end without an editor: runprintf '\e[>1u'to push the disambiguate flag, press Esc — the shell shows^[[27uinstead of a bare escape — thenprintf '\e[<u'to pop it back. Back at the bash prompt (no flag pushed) everything types as before; the mode is the program's to turn on, off by default (§25).
13. Copying, confirmed. Click the copy button in the files pane header, then in the folder-tree header, then the one on a selected entry's details popup. Each should raise a toast at the bottom of the window that fades on its own after about three seconds, and each should paste back what it promised — the pane's directory, the tree's folder, and the whole details card (name, target, type, time, size, owner). The context menus' Copy… items should raise the same toast.
14. Resuming where you left off. With a session open, cd /etc/ssh in the shell, point
the pane at a different directory (/tmp via the tree), toggle .* on, and drag both
splitters to unusual sizes. Then Disconnect and reconnect to the same target from the
home screen. Expect: the shell replays a visible cd /etc/ssh, the pane reopens on /tmp
(and does not get dragged to /etc/ssh by the shell's first announcement), the tree has
revealed the chain down to it, .* is still on, and both panels are the size you left them.
Kill the connection the hard way too (docker stop cmote-sshd, or pkill sshd on the
remote) and reconnect — the snapshot should survive a hangup, not just a clean disconnect.
Finally, connect to a target saved by an older build (or delete the session fields from
targets.json by hand): it should open at the login directory with default panels, no error.
15. Tabs. Connect a session, then click "+" on the strip: a new Home tab opens and
takes over the window while the first shell keeps running behind it. Connect a second target,
then click back to the first tab — its shell should be exactly where you left it (run a slow
command like sleep 5; date in the background tab, switch away and back, and its output should
have arrived). Resize the window with two live tabs and switch between them: each grid should
fit the window (no missing bottom row). Click a tab's "×": an idle Home/Connect tab closes at
once, a live shell asks to confirm first. A rename or delete on one tab's home list
should be visible on another tab's home list (the target store is shared).
16. Quitting cleanly. With one tab left, click its "×" (or, from the home screen, press
Ctrl+D): instead of reopening a blank tab, cmote asks Quit cmote? — Cancel keeps the
window, Quit exits. With a live session, the dialog says how many will disconnect; on Quit
the shell should be torn down cleanly (who / the server's log shows a normal logout, not a reset)
before the window closes. Now the title-bar ×: it too asks Quit cmote? — even with no live
session — and on confirm disconnects everything before exiting. While the Quit cmote? card is up,
drag it by its header — like every other dialog it moves and stays where you drop it (clamped so
the header never leaves the window); the same works on the per-tab live-shell close card. On a
live shell, Ctrl+D should reach the remote as EOF (it logs you out, landing back on the home
screen), not close the tab; a second Ctrl+D there then closes it.
17. Port forwards. In a live session click Tunnels on the status bar. Add a Local
forward — listen 8080, to localhost:22 (or any service the remote can reach) — and expect the
row to go from ○ to a green ●; from your machine, curl localhost:8080 (or ssh -p 8080 localhost) should reach it through the tunnel. Add a Dynamic forward — listen 1080 — and
point a browser or curl --socks5 localhost:1080 https://example.com at it. Add a Remote
forward — listen 9090 on the server, to localhost:<something-local> — and connect to it on the
remote. Try a duplicate bind (two locals on 8080) and a taken port: the first is refused inline,
the second shows the row failed without dropping the shell. Remove a forward with its ✕ — the
tunnel stops. Then Disconnect and reconnect to the same target: the forwards you left should be
re-established automatically (they are saved in targets.json).
Cleanup:
docker rm -f cmote-sshd
rm -f smoke_key smoke_key.pub smoke_key_enc smoke_key_enc.pubMIT — see LICENSE.
Bundled fonts keep their own licenses (redistributed under them): Fira Mono and IBM Plex Mono under the SIL Open Font License 1.1, and Material Icons under Apache-2.0 — each with its license text in assets/.