36 KiB
Rust port plan: Nexus Terminal, a native egui app with no Go and no webview
Status (11 October 2026): converted. Every phase below has been carried out on the
rust-portbranch; Go, Node and Wails are gone. What was verified and what wasn't:
- Verified here (Linux): the Go-generated fixtures (database, vault,
.linkexport, recording) load; SSH against a real OpenSSH server (key and agent auth, PTY shell, exec, local/remote forwards, SFTP, jump host, agent forwarding, host-key rejection), including through the GUI with the host-key prompt and login scripts; local shell, RAW, Telnet/RAW/serial client tests (serial on a pty pair); the session manager (events, reconnect backoff, restart of an exited local shell); the terminal renderer (colours, 256/true colour, CJK fallback); every window rendered headlessly;cargo clippyclean; the Windows target builds and links.- Not verified: macOS builds (need Apple's C toolchain), running on Windows or macOS, the OS keychain on a real desktop session (none was available, so the go-keyring compatibility in §3 is untested), IRC, GitHub and VNC against real servers (tested against fakes), serial hardware.
- Update URLs: moved to
https://dixon.cx/downloads/nexus(§7).
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:
Tokensthemes, 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 oneCmdenum. 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).
- the hidden Leviathan mode (
- 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 | none (our own client over std TCP, rustls for TLS) |
Registration, PING, CTCP, NickServ via PRIVMSG; reconnect and channel-state persistence are ours |
| VNC | none (our own RFB client; des, flate2, image for Tight JPEG) |
RFB 3.3–3.8, VNC auth, Raw/CopyRect/Hextile/ZRLE/Tight → 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 (pure-Rust D-Bus via zbus on Linux) |
Must read entries written by go-keyring (§3) |
| TOTP | none (hmac + sha1, ~30 lines) |
RFC 6238, normalised like pquerna/otp |
| 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
Servicekeeps the same method names as today'sapi.Service(connect_session,sftp_list, …), andEventis 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<Event>.Servicemethods that touch SQLite are fast and run inline (rusqlite behind aMutex<Connection>, 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 (
serialportandportable-ptyhave blocking APIs). Each session has anmpscoutbound queue (keystrokes, resize, break, DTR/RTS). Inbound bytes are batched per read and sent as oneEvent::SessionData, followed byctx.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 orspawn_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
.linkexports- 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 fromexportimport.go. Only the extension changes (.link→.nexus). -
Keychain: ⚠ go-keyring and
keyringdon'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, butkeyringdefaults touser.service. UseEntry::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.sqladdsserialto the protocol CHECK, plus aserial_configJSON 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 becauseserialportdoesn't support them. If they're needed, add them throughtermios/DCBon the raw handle (about 30 lines per OS). - Device path: stored in
host, withport0, 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/uucpgroup on Linux,/dev/cu.*on macOS, and busy ports - a Help topic
5. Toolchain and platforms
- Toolchain: pin the same
rust-toolchain.tomlas 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::Termper view, behind aFairMutex, fed byvte::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.textcall 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 todefaultScrollback(0–100000). - Input:
- egui
Keyevents map to escape sequences, respecting the terminal mode (application cursor keys, keypad). Event::TextandEvent::Ime(Commit)send the text, so IME input works.- Paste is bracketed when the application enables it.
- Output goes to
write_sessionand to the broadcast targets.
- egui
- 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.resizeandresize_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:
catof 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, feedTermon the session thread and keep only drawing on the UI thread (theFairMutexalready 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
As built: vnc-rs was not used; the fallback below (our own RFB client) is what shipped.
vnc-rson a tokio task, with an optional SSH tunnel through §2'sdirect-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 fromvnc.js). - If
vnc-rsdoesn'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 theimagecrate.
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/… |
dixon.cx/downloads/nexus/… |
| 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):
- If
NexusTerminal/nexus.dbexists, use it. This covers users from before the rebrand, whose data was never moved. - Otherwise, if
AlchementaryLink/link.dbexists, copy (don't move)link.db→nexus.db, plus thevault/directory and the recordings, intoNexusTerminal/. Write amigrated-frommarker. The old directory is left untouched, so the Wails build still works if a user rolls back. - Keychain: when a read under
NexusTerminalmisses, tryAlchementaryLink. If it's found there, write it underNexusTerminal. Old entries are not deleted. - 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 byrelease.shinCargo.toml. - Update URL:
option_env!("NEXUS_UPDATE_URL"), defaulting to the URL decided in §7. This replaces Go's-ldflags -X.
- Version:
- Packaging: Leviathan's
packaging/layout, renamed:- macOS: a universal
NexusTerminal.appviabuild-app.sh, with Developer ID signing and notarisation. - Windows: an
.exewith an icon and version info frombuild.rs/winresource, using the GUI subsystem. - Linux: a tarball, plus
install.shand a.desktopfile.
- macOS: a universal
- CI: rewrite
release-builds.ymlwithdtolnay/rust-toolchainand a native runner per OS and arch. Drop Go, Node, Wails andlibwebkit2gtk. Runcargo teston every platform. - Unchanged: the update server (
updates/index.php) and the manifest format.release.shgets 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/errortoasts,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
ListSSHKeysautocomplete, 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.dbis used as-is - Keychain entries under
AlchementaryLinkare found and re-saved underNexusTerminal - Import accepts
.linkand.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 testinnexus-core:- Compatibility: every §3 fixture read and round-tripped: database, vault,
.linkexports, 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,rawclientencoding and history,modelsdefaults, andupdatermanifest 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.
- Compatibility: every §3 fixture read and round-tripped: database, vault,
- SSH integration: a CI job against an
openssh-servercontainer, 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.pngflag, and check each view underxvfb-runin CI. test-regressions.jsis retired; §10 replaces it.