From 7df353a6eba07ad94b90da3f01e80b195e0e7227 Mon Sep 17 00:00:00 2001 From: Andy Dixon Date: Sat, 10 Oct 2026 12:56:36 +0100 Subject: [PATCH] docs: Rust port plan (Nexus Terminal) --- docs/rust-port-plan.md | 464 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 464 insertions(+) create mode 100644 docs/rust-port-plan.md diff --git a/docs/rust-port-plan.md b/docs/rust-port-plan.md new file mode 100644 index 0000000..9965ce0 --- /dev/null +++ b/docs/rust-port-plan.md @@ -0,0 +1,464 @@ +# Rust port plan: Nexus Terminal, a native egui app with no Go and no webview + +Goal: rebuild Alchementary Link in Rust as **Nexus Terminal**, a single Rust application in the +style of Leviathan Desktop (`../leviathan/rust`). +- **UI:** the Wails webview (HTML/CSS/JS, xterm.js) is replaced by an **eframe/egui 0.36** shell with + Leviathan's design system: `Tokens` themes, Inter and JetBrains Mono, Phosphor icons, a custom title + bar, a navigator rail, document tabs, a dock, a status bar, and the Ctrl/Cmd+K palette over one `Cmd` + enum. Full functional parity is required. +- **Backend:** the Go core is ported to Rust. At the end, Go, Node and Wails are gone from the repo. +- **Rename:** back to **Nexus Terminal** (§7). Existing Alchementary Link data is migrated on first + run. +- **Removed:** + - the hidden **Leviathan** mode (`internal/leviathan/*`, `leviathan.js`, the Konami toggle). It now lives in Leviathan Desktop. + - the **VS Code bridge** (`cmd/vscode-bridge`). +- **New:** a Serial protocol (§4). +- **VNC is included, not deferred.** Rust has a usable RFB client crate (§6a). + +### Crates + +| Need | Crate | Notes | +|---|---|---| +| UI | `eframe`/`egui` 0.36, `egui-phosphor`, `rfd`, `image` | Same versions and features as Leviathan (`glow`, `wayland`, `x11`, `accesskit`, `persistence`) | +| Async runtime | `tokio` (multi-thread, 2 workers) | Network sessions only. The UI thread never blocks on it | +| SSH, SFTP | `russh`, `russh-sftp`, `ssh-key` | Strict-kex (Terrapin) support; client-side **agent forwarding** (`server_channel_open_agent_forward`); `direct-tcpip` for jump hosts; `tcpip-forward` for remote forwards | +| Terminal emulation | `alacritty_terminal` | VT parser, grid, scrollback, selection and regex search (the same engine Zed uses). We draw it with egui's painter (§6) | +| Local shell | `portable-pty` | `forkpty` on Unix, ConPTY on Windows | +| Serial | `serialport` | Baud/bits/parity/stop, RTS/CTS and XON/XOFF, DTR/RTS, break, port enumeration | +| IRC | `irc` (tokio, rustls) | Registration, PING, CTCP, NickServ via PRIVMSG; reconnect and channel-state persistence are ours | +| VNC | `vnc-rs` | RFB handshake, VNC auth, Raw/CopyRect/Tight/ZRLE → egui texture | +| HTTP (GitHub, updater) | `ureq` 3 | Blocking calls on `spawn_blocking` | +| SQLite | `rusqlite` (`bundled`) | SQLite compiled in, so no system library is needed | +| Vault, export | `argon2`, `aes-gcm`, `rand` | Native Argon2id at 128 MiB takes about 0.3 s | +| Keychain | `keyring` 3 | Must read entries written by go-keyring (§3) | +| TOTP | `totp-rs` | | +| JSON | `serde`, `serde_json` | Protocol configs stay JSON columns | +| Telnet, RAW, recordings | none (tokio `TcpStream`, serde) | Small ports of the Go code | + +There are **no system C libraries**. SQLite is compiled in, and TLS uses rustls. The Linux binary needs +only the X11/Wayland/GL libraries that eframe loads, as Leviathan does. + +## 1. Architecture + +Like Leviathan, the project is engine-first: a UI-free core crate, and the egui app is one client of it. + +``` +rust/ +├── core/ nexus-core: storage, secrets, models, protocol clients, SessionManager, Service. No egui. +└── app/ nexus: the egui shell +``` + +- **`Service`** keeps the **same method names** as today's `api.Service` (`connect_session`, + `sftp_list`, …), and **`Event`** is one enum whose variants match today's event names + (`SessionData`, `SessionState`, `SecretPrompt`, `HostKeyVerify`, …). The checklist in §10 refers to + these names. They are now plain Rust calls with no serialisation. +- **The UI thread** runs egui and owns all UI state. Each frame it drains a + `crossbeam_channel::Receiver`. `Service` methods that touch SQLite are fast and run inline + (rusqlite behind a `Mutex`, in WAL mode). +- **One tokio task per session** (SSH, Telnet, RAW, IRC, GitHub refresh, VNC). Serial and local-shell + sessions each get a blocking thread (`serialport` and `portable-pty` have blocking APIs). Each session + has an `mpsc` outbound queue (keystrokes, resize, break, DTR/RTS). Inbound bytes are batched per read + and sent as one `Event::SessionData`, followed by `ctx.request_repaint()`. Sending raw bytes instead + of base64 JSON is the main performance gain over the Wails app. +- **Credential prompts:** when a session task needs a secret or a host-key decision while connecting, + it sends `Event::SecretPrompt { reply: oneshot::Sender }` and awaits the answer. Closing the session + drops the receiver, which cancels the prompt. This is the same contract as the Go channel. +- **Long jobs** (SFTP transfers, archive download, `test_connection`, key generation) are tokio tasks + or `spawn_blocking`, and report progress through events. + +## 2. SSH (russh) + +| Feature | Approach | +|---|---| +| Interactive shell | `channel.request_pty(term type from the profile)`, env vars, `request_shell`, `window_change` | +| Auth | password, public key (files, with passphrase), agent (`SSH_AUTH_SOCK`; Pageant and the Windows OpenSSH pipe through russh's agent client), keyboard-interactive, with TOTP answering the prompts | +| Host keys | `Handler::check_server_key` against **our** SQLite known-hosts table (strict/ask/accept-new/insecure). Never `~/.ssh/known_hosts` | +| Keepalive | russh `keepalive_interval`/`keepalive_max` from the profile | +| Jump hosts | Hop N opens `channel_open_direct_tcpip`, and its stream is passed to hop N+1 as the transport (`connect_stream`). No loopback socket is needed | +| SOCKS5 and HTTP proxies | `tokio-socks` for SOCKS5; HTTP CONNECT is about 40 lines. The resulting stream goes to `connect_stream` | +| Local forwards | A `TcpListener` per forward → `channel_open_direct_tcpip` → `copy_bidirectional` | +| Remote forwards | `tcpip_forward`, plus `Handler::server_channel_open_forwarded_tcpip` | +| Agent forwarding | `agent_forward` request plus `server_channel_open_agent_forward`, bridged to the local agent. ⚠ Prove this in the phase 0 spike | +| SFTP | `russh-sftp` on a second channel: list/stat/mkdir/rename/remove/upload/download, with progress events | +| Key manager | `ssh-key`: generate ed25519, RSA 2048/4096 and ECDSA; write OpenSSH private keys (encrypted with bcrypt-pbkdf + aes256-ctr); public-key export; `file_is_private_key`. The key comment becomes `name@nexus-terminal` | +| `~/.ssh/config` import | Port of `sshconfig` (~300 lines of Go) | +| Compression | russh `zlib@openssh.com` when the profile enables it | + +The fallback, if a russh gap blocks the spike, is `ssh2` (libssh2 with vendored OpenSSL). It has no +API for accepting agent-forwarding channels, and it brings C back into the build. + +## 3. Porting the Go backend + +Sizes are Go lines, excluding tests. The Go tree is 41k lines. Leviathan (25.5k), VNC's Go side and the bridge are not +ported, which leaves about 15k lines to port. + +| Go package | Lines | Rust replacement | +|---|---|---| +| `storage` + `migrations/*.sql` | 949 | rusqlite. **Embed the same `.sql` files** with `include_str!` and keep the same migrations table, so existing databases open unchanged | +| `models`, `validation`, `preferences`, `audit`, `knownhosts`, `config` | ~1.5k | Plain modules. Protocol configs stay JSON columns, read as `serde_json::Value` or, where the UI edits them, as `#[derive(Deserialize)]` structs with `#[serde(default)]`. Leviathan preferences (`leviathanEnabled`, …) are ignored on read | +| `secrets` (vault + keychain) | 460 | Vault: `argon2` + `aes-gcm`. Keychain: `keyring`, with explicit target names to match go-keyring (see below) | +| `exportimport` | 209 | Same envelope and the same crypto; `.nexus` extension (§7) | +| `sessions` + `api` | ~6k | `SessionManager` + `Service` (§1): reconnect with attempts and backoff, login scripts, broadcast, recording hooks, secret resolution (saved / session-only / ask every time). The Leviathan methods are dropped | +| `sshclient`, `sftpclient`, `sshconfig`, `terminalenv` | ~1.7k | §2 | +| `telnetclient` | 603 | tokio `TcpStream` and the option state machine (echo, SGA, NAWS, TTYPE, BRK) | +| `rawclient` | 561 | tokio; history, capture, and hex/escaped encoders | +| `ircclient` (on `girc`) | 833 | `irc` crate plus our own state: rooms, NAMES, LIST, topic, reconnect, channel-state persistence (migration 006) | +| `githubclient` | 408 | `ureq` + serde; periodic refresh; archive download | +| `localshell` | 294 | `portable-pty` | +| `recording` | 538 | asciicast v2 writer and reader; the files are the same | +| `updater` | 303 | `ureq`; same manifest format; URLs per §7 | +| `vncclient` | 340 | §6a | +| `leviathan/*` | 25.5k | **Deleted.** | +| `cmd/vscode-bridge` | 357 | **Deleted.** | + +### Data compatibility (hard requirement) + +Existing users upgrade in place. **Before any Go is deleted**, generate fixtures from the Go code and +commit them under `rust/core/tests/fixtures/`: +- a database with every protocol, snippets and known hosts, migrated through 006 +- a vault file with a known password +- `.link` exports +- recordings +- keychain entries written by go-keyring, on each OS in CI + +Rust must read every fixture byte for byte. The formats are: +- **Vault:** JSON `{version, salt(b64, 32 bytes), secrets: key → b64(nonce12 ‖ ciphertext ‖ tag)}`; + Argon2id with t=3, m=128 MiB, lanes=4, and a 32-byte key. +- **Export:** `[1 byte version][32 bytes salt][12 bytes nonce][ciphertext]`, with the Argon2 parameters + from `exportimport.go`. Only the extension changes (`.link` → `.nexus`). +- **Keychain:** ⚠ go-keyring and `keyring` don't store entries the same way by default: + - **macOS:** both use a generic password stored under service and account, so they match. + - **Windows:** go-keyring's target is `service:user`, but `keyring` defaults to `user.service`. Use `Entry::new_with_target`. + - **Linux:** both use the Secret Service, but with different attribute sets. Search by `service` + `username`. + + The phase 0 spike proves all three against the fixtures. + +## 4. New protocol: Serial + +- **Library:** `serialport`. +- **Migration:** `007_serial_protocol.sql` adds `serial` to the protocol CHECK, plus a `serial_config` JSON column. +- **Config fields and defaults:** `baudRate 115200, dataBits 8, parity none|odd|even, stopBits 1|2, + flowControl none|xonxoff|rtscts, lineEnding cr|lf|crlf, localEcho false, dtrOnConnect true, + rtsOnConnect true, encoding, autoReconnect true, maxReconnectTries 5`. + Mark/space parity and 1.5 stop bits are left out because `serialport` doesn't support them. If they're needed, add them + through `termios`/`DCB` on the raw handle (about 30 lines per OS). +- **Device path:** stored in `host`, with `port` 0, so the sidebar, search and palette work unchanged. +- **Session:** a terminal session through `TermView`, so recording, triggers, broadcast, find, snippets + and split panes all work. Unplugging the adapter sets the session to disconnected, and auto-reconnect re-opens the path. + New service methods: `list_serial_ports`, `set_serial_lines(dtr, rts)`, `send_break`. +- **UI:** + - an editor page (device combo with refresh, line settings, behaviour settings) + - a Quick Connect entry + - Send Break and DTR/RTS toggles on the tab + - a badge colour + - error hints: the `dialout`/`uucp` group on Linux, `/dev/cu.*` on macOS, and busy ports + - a Help topic + +## 5. Toolchain and platforms + +- **Toolchain:** pin the same `rust-toolchain.toml` as Leviathan (`1.99.0`), edition 2024. +- **Targets:** keep the current release matrix in full: darwin amd64 and arm64 (one universal `.app`), + linux amd64 and arm64, and windows amd64 and arm64. +- **UI code from Leviathan:** copy these files into `app/src/` once, then let them evolve + independently. There is no dependency on the Leviathan repo: + - `theme.rs` (Tokens, fonts; 278 lines) + - `widgets.rs` (198 lines) + - the title bar, command palette and frame layout from `app.rs` + - `hexview.rs`, for the RAW hex view + - bundled fonts + - the `packaging/` layout + +## 5a. UI mapping: webview module → egui + +egui is immediate mode. Lists are virtualised with `ScrollArea::show_rows`, so the 1000/200/5000-row caps go away. +Terminal views are repainted only when their session receives data, and for input and cursor blink. + +| Current (JS) | egui replacement | +|---|---| +| `index.html` grid layout | Leviathan's frame: custom title bar (Nexus mark, menus, active-session title, ⌘K launcher, window controls); a **navigator rail** holding the connection sidebar (resizable 180–500); a central **tab strip + workspace** with an empty state; a right **dock** for SFTP and the profile editor; a **status bar** | +| Shortcuts (`app.js`) | The `Cmd` enum plus a shortcut table. `Modifiers::COMMAND` maps to ⌘ on macOS and Ctrl elsewhere. The palette enumerates every `Cmd` | +| `tabs.js` | A painted tab strip (Leviathan's tab style) with a protocol badge, status dot, activity marker, pin and 📁; pinned tabs first; drag to reorder; scroll with edge fades; context menu; tooltip with "Connected since"; close confirmation | +| `terminal.js` + xterm.js | `TermView` (§6) | +| `splitpane.js` | A binary tree of `Pane::{Leaf(session), Split{dir, ratio, a, b}}` laid out by rect, with draggable dividers (minimum 100 px). A placeholder leaf shows the connection picker. Child sessions get no tab | +| `sidebar.js` | Collapsing sections (Local Machine, ★ Favourites, Recent 5, folders, Ungrouped), a protocol dot and ★, Ctrl/Cmd-click multi-select, a context menu, a bulk-action bar, and fuzzy search | +| `profiles.js` drawer | A dock panel: protocol selector plus one section per protocol. List editors (jump hosts, forwards, RAW presets, login script) are `egui::Grid` rows with ▲▼✕ | +| `settings.js` | Settings window with tabs: General, Terminal, Security, Known Hosts, Help, About | +| `secrets.js` | Modal windows: secret prompt, master password (strength meter using the same formula), host-key dialog | +| `sshkeys.js` | SSH Keys window: list, generate, copy public key, delete | +| `sftp.js` | A dock panel per session: breadcrumb, table, bookmarks, and a transfer queue with progress bars. Dialogs via `rfd` | +| Drop files on terminal | `ctx.input(\|i\| i.raw.dropped_files)` gives real paths. The File Drop dialog stays. Upload by path, so the 768 KiB base64 chunking goes away | +| `raw.js` | Text view, hex view (the copied `hexview`, read-only), or split; a send bar (format combo, input, quick-byte buttons, presets); Save via `rfd` | +| `irc.js` | Room tabs + a read-only `TermView` fed by the ported mIRC→ANSI formatter + a composer + side lists (rooms, room list, users) | +| `github.js` | Painted repo cards under group headers; a filter; Refresh; ZIP and Visit buttons | +| `vnc.js` | `VncView` (§6a) | +| `commands.js` | The copied palette widget, with Nexus's sections and a port of `fuzzyMatch` | +| `snippets.js`, `triggers.js`, `recordings.js` | Windows. Playback uses its own `TermView`, with a scrubber, speed control and autoplay | +| `notifications.js` | A toast stack painted top-right in an `egui::Area`, auto-dismissed after 4 s (6 s for errors). About 60 lines; no crate needed | +| `themes.js` | App chrome uses `Tokens` (Pro/Midnight/Light). `terminal_themes.rs` is a const table of the 11 terminal palettes (charcoal … rose-pine) | +| Help tab HTML | The help topics minus Leviathan, as `include_str!` text with simple heading and paragraph styling | +| `BrowserOpenURL`, clipboard, fullscreen | `ctx.open_url`, `ctx.copy_text` and egui paste events, `ViewportCommand::Fullscreen` | + +**App theme:** every widget is drawn by egui, so the theme applies everywhere. + +## 6. TermView (the critical path) + +- **Model:** one `alacritty_terminal::Term` per view, behind a `FairMutex`, fed by + `vte::ansi::Processor`. Session data events are fed straight in as bytes; UTF-8 is the parser's job. +- **Drawing:** for each visible row, group cells into runs with the same attributes, then draw one + `painter.text` call per run, plus background rectangles, cursor, selection and search highlights. + Cache each row's layout by a hash of its content. Use the profile's font, falling back to the + preferences (JetBrains Mono is bundled). Wide and emoji cells take the terminal's cell width. +- **Scrollback:** `Config.scrolling_history`, from the profile, falling back to `defaultScrollback` + (0–100000). +- **Input:** + - egui `Key` events map to escape sequences, respecting the terminal mode (application cursor keys, keypad). + - `Event::Text` and `Event::Ime(Commit)` send the text, so IME input works. + - Paste is bracketed when the application enables it. + - Output goes to `write_session` and to the broadcast targets. +- **Mouse:** + - When the application enables mouse reporting, send SGR/X10 reports. + - Otherwise: drag selects, double-click selects a word, triple-click selects a line. + - The wheel scrolls the scrollback. On the alternate screen it sends ↑/↓ once per notch (the existing tmux behaviour). + - Ctrl/Cmd-click on a URL opens it. +- **Behaviours to keep:** + - **Clipboard:** Ctrl/Cmd+C copies when there is a selection and otherwise sends SIGINT; Ctrl/Cmd+V pastes. + - **Resize:** debounced; recompute columns and rows, then `term.resize` and `resize_session`. + - **Right-click menu:** all 11 items. + - **Find bar:** alacritty `RegexSearch`, with case and regex options, "i of N", Enter and Shift+Enter. + - **Overlays:** the disconnect overlay with Reconnect, and the local shell's "[Exited - press enter to restart]". + - **Cursor:** style (block, underline or bar) and blink. +- **Performance target:** `cat` of a 50 MB file stays responsive, at parity with or better than xterm.js. + Parsing runs on the UI thread inside the event drain. If profiling shows that's too slow, feed `Term` on the session + thread and keep only drawing on the UI thread (the `FairMutex` already allows this). +- **Reference:** `egui_term` (alacritty_terminal + egui) is prior art. Read it, but don't depend on it, + because it lags egui releases. + +## 6a. VNC + +- `vnc-rs` on a tokio task, with an optional SSH tunnel through §2's `direct-tcpip`. +- Decoded rectangles are applied to an RGBA buffer, and dirty frames update a `TextureHandle`, scaled to fit. +- Input: pointer → `PointerEvent`; keys → X11 keysyms (port the keysym map from `vnc.js`). +- If `vnc-rs` doesn't work with the servers users run, the fallback is our own RFB client: Raw, CopyRect and ZRLE + in about 600 lines, plus Tight's JPEG via the `image` crate. + +## 7. Rename to Nexus Terminal and migrate data + +The rebrand to Alchementary Link (commit `1a380a0`) renamed every path and **migrated nothing**. This rename +undoes it and migrates the data. + +| Item | Alchementary Link (now) | Nexus Terminal | +|---|---|---| +| Product and window title | Alchementary Link | Nexus Terminal | +| Binary, bundle and app id | `AlchementaryLink` | `NexusTerminal` / `nexus-terminal` | +| Config dir | `…/AlchementaryLink` | `…/NexusTerminal` | +| Database and log | `link.db`, `link.log` | `nexus.db`, `nexus.log` | +| Keychain service | `AlchementaryLink` | `NexusTerminal` | +| Export file | `.link`, "Link Export" | `.nexus`, "Nexus Export" (import accepts both) | +| Updater User-Agent and GitHub fallback | `AlchementaryLink-UpdateChecker`, `alchementary-link` | `NexusTerminal-UpdateChecker`, `nexus-terminal` | +| Update and release URLs | `alchementary.co.uk/download/link/…` | ⚠ **Decision needed:** the new URL for the manifest and downloads | +| Icons and brand mark | `alchementary-link-*.png`, ">_ AlchementaryLink" | Restore the `nexus-terminal-*` icons from git history (pre-`1a380a0`), redrawn for the egui title bar | + +**First-run migration** (in `nexus-core`, before the database is opened): +1. If `NexusTerminal/nexus.db` exists, use it. This covers users from before the rebrand, whose data was never moved. +2. Otherwise, if `AlchementaryLink/link.db` exists, **copy** (don't move) `link.db` → `nexus.db`, plus the + `vault/` directory and the recordings, into `NexusTerminal/`. Write a `migrated-from` marker. + The old directory is left untouched, so the Wails build still works if a user rolls back. +3. **Keychain:** when a read under `NexusTerminal` misses, try `AlchementaryLink`. If it's found there, write it under + `NexusTerminal`. Old entries are not deleted. +4. If both directories hold a database (a user who ran both builds), Nexus wins, and the toast says so. + +The old update channel must point existing Alchementary Link installs at the first Nexus release. That's one +manifest entry on the update server, so existing users get the Rust build. + +## 8. Phases and milestones + +Each phase ends with a runnable build and its slice of the §10 checklist ticked off. **The Wails app keeps +shipping until phase 7**, and Go is deleted only then. + +| # | Phase | Delivers | Milestone | +|---|---|---|---| +| 0 | Fixtures and spikes | The Go-generated fixtures (§3). Spikes, each answering one question: (a) a russh PTY shell against OpenSSH with an ed25519 key, strict-kex, a jump host and **agent forwarding**; (b) `TermView` on alacritty_terminal with vim, tmux, htop, IME input, and `cat` of 50 MB; (c) `keyring` reads go-keyring entries on all three OSes; (d) `vnc-rs` against TigerVNC and macOS Screen Sharing | Go/no-go on each risk in §2, §3 and §6a | +| 1 | Core foundation | `nexus-core`: storage and migrations, models, validation, preferences, audit, known hosts, vault, keychain, export/import, `Service`/`Event`, and the **§7 migration**. App skeleton in Leviathan's frame: rail, sidebar, tabs, status bar, toasts, and the secret, master-password and host-key modals | Opens an existing Alchementary Link user's data as Nexus; profiles listed; lock and unlock | +| 2 | Terminal | `TermView` (§6), terminal themes, local shell, Telnet, **Serial**, state handling (reconnect toast, error classifier, "Connection Already Open") | A usable local, Telnet and serial terminal | +| 3 | SSH | §2 in full, plus SFTP and the key manager | Usable as a daily SSH client | +| 4 | Forms | Profile editor (every field in §10.3), Quick Connect, Settings, Known Hosts, SSH Keys, encrypted export/import, update check | All profile CRUD and settings | +| 5 | Power features | Split panes, broadcast, snippets, triggers, recording and playback, port-forward dialog, SFTP dock and file drop, palette, shortcuts, fullscreen | Every terminal-session feature | +| 6 | RAW, IRC, GitHub, VNC | Rust clients and their views | Every protocol | +| 7 | Ship; delete Go | Packaging and CI (§9). Delete `go.mod`, `go.sum`, `main.go`, `internal/`, `cmd/`, `frontend/`, `wails.json`, the Wails `build/` assets, and the Go build scripts (`build*.sh`, `build.ps1`, `Makefile`, `build-all.sh`). Rewrite the docs, README and `.github/copilot-instructions.md` as Nexus Terminal. Publish the update-server entry that moves users across (§7) | Nexus Terminal released from a tag; no Go or JS left | + +## 9. Build and release + +- **Build:** `cargo build --release --target …`. + - Version: `CARGO_PKG_VERSION`, set by `release.sh` in `Cargo.toml`. + - Update URL: `option_env!("NEXUS_UPDATE_URL")`, defaulting to the URL decided in §7. This replaces Go's `-ldflags -X`. +- **Packaging:** Leviathan's `packaging/` layout, renamed: + - **macOS:** a universal `NexusTerminal.app` via `build-app.sh`, with Developer ID signing and notarisation. + - **Windows:** an `.exe` with an icon and version info from `build.rs`/`winresource`, using the GUI subsystem. + - **Linux:** a tarball, plus `install.sh` and a `.desktop` file. +- **CI:** rewrite `release-builds.yml` with `dtolnay/rust-toolchain` and a native runner per OS and arch. + Drop Go, Node, Wails and `libwebkit2gtk`. Run `cargo test` on every platform. +- **Unchanged:** the update server (`updates/index.php`) and the manifest format. `release.sh` gets the new names. + +## 10. Parity checklist + +This is the acceptance list, built from a full read of `frontend/src/js/*`. A feature is done when it works in the native build. + +### 10.1 Shell, tabs, sessions +- [ ] Layout: sidebar, tab strip (+ New, Broadcast: left-click toggles, right-click picks targets), workspace empty state (New Connection, Quick Connect), status bar +- [ ] Sidebar footer: New, ⌘K, Import, Export, Recordings, Settings +- [ ] Shortcuts: ⌘K, ⌘N, ⌘W, ⌘Tab/⇧⌘Tab (wrap), ⌘1–9, ⌘, (Settings), ⌘F, ⇧⌘D, ⇧⌘E, ⇧⌘R, ⇧⌘B, ⇧⌘S, ⇧⌘Q, F11; Esc closes palette and exits fullscreen +- [ ] Events handled: `session:data`, `session:state` (with attempt/maxRetries/delaySec toast), `session:closed`, `session:raw:data`, `session:github:*`, `session:irc:event`, `session:forward:started/stopped/error` toasts, `session:recording:started/stopped`, `session:sftp:progress`, `session:secret:prompt`, `session:hostkey:verify`, `app:open-about` +- [ ] Connect flow: "Connection Already Open" (Cancel / New Connection / Use Existing), "Connecting to X…" toast, refit and resize at 50/300/800 ms after connect +- [ ] Error classifier: auth errors → Close / Edit Profile / Retry; host-key errors also offer Known Hosts +- [ ] Tabs: + - [ ] Visuals: protocol badge, status dot, activity marker cleared on activate, pinned-first order + - [ ] Interaction: drag reorder; wheel scroll with overflow fades; scroll the active tab into view; hover hint (name + "Connected since"); close activates the nearest neighbour + - [ ] Context menu: Rename, Duplicate, Pin/Unpin, Move Left/Right, Reconnect, Close + - [ ] Close confirm when the pref is on (not for GitHub tabs) +- [ ] Session close is idempotent and cleans up splits, SFTP, triggers, recordings and viewers +- [ ] Local shell: `ConnectLocal`, exit and restart-on-Enter +- [ ] Quick Connect: SSH/Telnet/RAW/IRC/Serial, default ports, username (SSH/IRC), nick (IRC), Enter/Esc +- [ ] Broadcast: toggle toast, target picker with Select All (all selected means null, i.e. all sessions) +- [ ] Status bar latency: <100 good, <250 warn, else bad; stops on error +- [ ] Toasts: 4 types, × dismiss, 4 s / 6 s auto-dismiss + +### 10.2 Terminal +- [ ] §6 in full; 11 themes; switching theme updates every open terminal +- [ ] Find bar (case, regex, count, next/prev, clear highlights) +- [ ] Context menu: all 11 items, with the shortcut labels for the platform +- [ ] File drop: Paste contents / Upload to current directory (SSH only) via the directory picker (breadcrumb, filter, Up, Refresh, READ ONLY / NO READ badges, "Use this folder" disabled when the directory isn't writable, keyboard navigation) +- [ ] Port Forwards dialog: list, Stop, add (L/R, bind, dest, label) +- [ ] Split panes: split the focused pane right/down, placeholder picker (Local Machine + profiles), divider drag with live refit, per-pane ✕ promotes the sibling, late connects to a closed pane are disconnected, closing the tab closes child sessions +- [ ] Recording toggle and a visible tab indicator + +### 10.3 Profiles, sidebar, settings, security +- [ ] Sidebar: + - [ ] Sections: Local Machine, Favourites, Recent 5, folders (sorted), Ungrouped / "All Connections"; collapsible + - [ ] Search: fuzzy over name, host, tags and protocol; empty states + - [ ] Mouse: click connects; Ctrl/Cmd-click multi-selects; double-click edits + - [ ] Context menu: Connect, Edit, Duplicate, Favourite toggle, Delete (confirm) + - [ ] Bulk bar: Connect all, Disconnect (confirm), Tags add/replace, Move Group, Clear +- [ ] Profile editor, common: + - [ ] Protocol switch keeps common fields + - [ ] Validation: host, port, IRC nick and rooms, IRC SSH tunnel + - [ ] Buttons: Test (Reachable — N ms), Delete (confirm), Create/Save, Save & Connect + - [ ] General fields: Display Name, Host, Port, Username, Group (autocomplete), Description + - [ ] Terminal fields: font, size 8–36, scrollback 0–100000, cursor style + - [ ] Tags and Notes +- [ ] SSH fields: + - [ ] Password mode, auth methods + - [ ] Key path with `ListSSHKeys` autocomplete, Browse, and checks (reject `~`, `FileExists`, `FileIsPrivateKey`); passphrase mode + - [ ] Host key policy (strict/ask/accept-new/insecure) + - [ ] Jump hosts list, port forwards list + - [ ] Startup command, env KEY=VALUE, login script (wait/send, timeout 1–120, reorder) + - [ ] Agent auth and forwarding + - [ ] TOTP: show/hide, Test (code shown 5 s), Save/Delete, placeholder when a secret is saved + - [ ] Proxy (none/socks5/http, host, port 1080, user, password) + - [ ] Advanced: terminalType, keepalive interval/count, compression, autoReconnect, maxReconnectTries 1–20 +- [ ] Telnet fields: localEcho, lineMode, terminalType, encoding, line ending, negotiationDebug, login script, reconnect +- [ ] RAW fields: encoding, line ending, showTimestamps, showDirection, payload presets (name/payload/format), timeouts, autoReconnect +- [ ] GitHub fields: API base URL, token storage, refresh interval 60–3600 +- [ ] IRC fields: nick, realName, TLS, server password and mode, NickServ, auto-join rooms, SSH tunnel, reconnect +- [ ] VNC fields: every current field, saved round-trip +- [ ] Settings: + - [ ] General: theme, default protocol, confirm on close/disconnect, status bar, sidebar width, local shell (Browse) + - [ ] Terminal: theme, font, size, scrollback, cursor + - [ ] Security: init/unlock/lock + - [ ] Known Hosts: table, Remove + - [ ] Help: 13 topics + - [ ] About: version, Check for Updates, View release, Import ~/.ssh/config +- [ ] Master password dialog: strength meter (Weak/Fair/Good/Strong), confirm match +- [ ] Secret prompt (Esc cancels → `cancelled:true`) +- [ ] Host key dialog (Reject / Accept & Save) +- [ ] SSH Key Manager: list (name, type, bits), copy public key, delete, generate (ed25519/rsa 2048|4096/ecdsa, passphrase) +- [ ] Encrypted export (`.nexus`; import also accepts `.link`) (profile checkboxes, password + confirm) and import (password, skip/overwrite/duplicate) +- [ ] Command palette: + - [ ] Sections: Connections, Actions, Tabs, Application, Security (lock/unlock), Terminal (Trigger Rules; split/clear/reset when a session is active) + - [ ] Fuzzy ranking, ↑↓ Enter Esc +- [ ] Snippets: search, CRUD (name, description, command, folder), send to the active connected session, `{{hostname|date|time|timestamp}}` built-ins plus prompted variables +- [ ] Triggers: CRUD; name, pattern, regex, case-sensitive, enabled; actions notify/highlight/both/sound; line-buffered matching with CSI stripped +- [ ] Recordings: + - [ ] Browser: list, storage info, delete, cleanup by age (7/30/90) and size (1 GB / 5 GB) + - [ ] Playback: play/pause, restart, scrubber seek (replay to the target), m:ss, speed 0.5–10×, autoplay + +### 10.4 SFTP, RAW, IRC, GitHub, VNC +- [ ] SFTP panel: + - [ ] Layout: toggle, resizable, breadcrumb, Go to path, New folder, Upload, Refresh, Close + - [ ] File list: folders first, `..` entry, extension icons, size with modified-time hint, loading/empty/error + Retry + - [ ] Actions: multi-select, Download (files), Rename (single), Delete (confirm) + - [ ] Bookmarks + - [ ] Transfer queue with progress; refresh when an upload is done; toast when the panel is closed +- [ ] RAW: + - [ ] Views: Text/Hex/Split, Pause/Resume, Clear, Save capture, byte counter, 5000-entry buffer + - [ ] Prefixes: timestamps and direction per the profile + - [ ] Send: Text/Hex/Escaped formats, NUL/CR/LF/ETX/EOT, presets, Enter sends, local echo of the sent entry +- [ ] IRC: + - [ ] Room tabs: Server tab, unread markers, 2000-line buffers + - [ ] Composer: /commands via `IRCHandleInput`, Tab nick completion, click a nick to insert `@nick` + - [ ] Room lists: join box, joined rooms with topic hint, room list (filter, click to join) + - [ ] Users list with prefixes stripped + - [ ] Formatting: all event types formatted as today, nick colour hash, full mIRC formatting (^B ^] ^^ ^_ ^V ^O ^C fg,bg ^D hex) +- [ ] GitHub: + - [ ] Status line, filter, Refresh + - [ ] Ordering: owner groups ordered by newest repo (own login first on ties) + - [ ] Card contents: Private/Fork/Archived badges, last commit and committer, description, language colour, stars, relative pushed time + - [ ] Actions: ZIP (save dialog → `DownloadGitHubRepoArchiveToPath`), Visit +- [ ] VNC: connect (direct or SSH-tunnelled), scaled framebuffer, mouse and keyboard, clipboard, disconnect overlay + +### 10.4a Serial +- [ ] Profiles: create, edit and duplicate, and export/import round-trip including `serialConfig` +- [ ] Editor: device list and refresh +- [ ] Line settings: all baud, data-bit, parity, stop-bit and flow-control options applied +- [ ] Session behaviour: + - [ ] data in the terminal + - [ ] local echo, line ending and encoding + - [ ] DTR/RTS on connect and as toggles + - [ ] Send Break + - [ ] XON/XOFF and RTS/CTS flow control honoured + - [ ] unplug → disconnected overlay; auto-reconnect when the device returns +- [ ] Quick Connect Serial; recording, triggers, broadcast, snippets, find and split panes all work on serial sessions +- [ ] Permission-denied and busy-port hints + +### 10.5 Rename and migration +- [ ] Every user-visible string, window title, bundle, binary and icon says Nexus Terminal +- [ ] First run migrates `AlchementaryLink/` → `NexusTerminal/`, copying the database, vault and recordings, and leaves the old directory intact +- [ ] Pre-rebrand `NexusTerminal/nexus.db` is used as-is +- [ ] Keychain entries under `AlchementaryLink` are found and re-saved under `NexusTerminal` +- [ ] Import accepts `.link` and `.nexus`; export writes `.nexus` +- [ ] An existing Alchementary Link install updates to Nexus through the update server + + +## 11. Existing quirks: default is to fix during the port + +| Quirk | Native default | +|---|---| +| RAW Save passes the capture content as a file path (`raw.js:217` → `SaveSessionCapture(sid, path)`) | Save dialog, pass the path (fixes the bug) | +| Rename prompt's OK button says "Connect" | "Rename" | +| Recording tab dot never shows (wrong selectors) | Indicator drawn by the tab strip | +| `#status-protocol` never filled | Show the active session's protocol | +| Adding or removing a jump host, forward or preset discards unsaved edits | Grid editing keeps them | +| `folder` dropped when switching protocol | Keep it | +| RAW decodes received data as Latin-1 | Decode as UTF-8 in text view (hex view unchanged) | +| SFTP bookmarks keyed by session id (lost on disconnect) | Key by profile id | +| Help says sidebar supports drag between groups (it doesn't) | Fix the Help text; use "Move Group" | +| Trigger "sound" uses a WebAudio 880 Hz beep | the system bell | + +Client-side state that is **not migrated**: trigger rules and SFTP bookmarks live in WebKit localStorage. Users recreate them; everything else is in the SQLite DB, the vault and the keychain, which are read as-is (§3). + +## 12. Tests + +- **Before Go is deleted:** the §3 fixtures. +- **`cargo test` in `nexus-core`:** + - **Compatibility:** every §3 fixture read and round-tripped: database, vault, `.link` exports, recordings, and the keychain on each OS. + - **Migration (§7):** old dir only → copied; new dir only → untouched; both → Nexus wins; keychain fallback. + - **Ported Go test tables:** `validation`, `secrets`, `storage`, `exportimport`, `sshconfig`, `knownhosts`, + `terminalenv`, `rawclient` encoding and history, `models` defaults, and `updater` manifest parsing. + - **Ported JS tests:** IRC mIRC→ANSI (`test-irc.js`), and split-tree lifecycle with late-connect + rejection (`test-session-lifecycle.js`). + - **Pure logic:** fuzzy match, snippet variables, trigger line buffering, and password strength. + - **Serial:** a pty pair on Linux, covering data, echo, line endings and XON/XOFF. +- **SSH integration:** a CI job against an `openssh-server` container, covering password, key, + keyboard-interactive with TOTP, jump host, local and remote forwards, agent forwarding, an SFTP round trip, + and a host-key mismatch. +- **Screenshots:** copy Leviathan's `--screenshot out.png` flag, and check each view under `xvfb-run` in CI. +- `test-regressions.js` is retired; §10 replaces it.