π Overview
Ternix is a privacy-first desktop terminal and remote-session manager. It speaks SSH, Telnet, Serial, local shells, RDP, and VNC from a single interface, and bundles the tools you normally reach for separately: an SFTP file manager, an SSH key vault, port forwarding, session recording, a live system monitor, and command snippets.
Everything is stored locally in a SQLite database. Credentials are encrypted at rest with AES-256-GCM. Nothing is sent to a cloud service.
It ships as a native app for Linux (AppImage/deb) and Windows (NSIS installer/portable).
β¨ Features
π Protocols
| Protocol | What Ternix does |
|---|
| SSH | Password, public-key, SSH-agent, keyboard-interactive (MFA/OTP), and none auth. Multi-hop jump-host chains (ProxyJump). Host-key pinning. Per-session keepalives and compression. Server banner display. Round-trip latency probe. Startup commands. Optional agent forwarding (under agent auth). |
| Telnet | Raw TCP with IAC option negotiation β NAWS (window size, re-sent on resize), terminal-type (xterm-256color), suppress-go-ahead, and echo. Auto-login on login: / password: prompt detection. |
| Serial / COM | Native serialport. Port enumeration with manufacturer names. Baud 300β921600, 7/8 data bits, 1/1.5/2 stop bits, parity (none/even/odd/mark/space), flow control (RTS/CTS or XON/XOFF). |
| Local shell | Native PTY via node-pty. Defaults to $SHELL (or /bin/bash) on Unix and $COMSPEC (or PowerShell) on Windows. Custom shell, args, cwd, and env supported. |
| VNC | Embedded in-pane viewer (noVNC) over a loopback, token-gated WebSocketβTCP bridge. VNC password auth. Scales and centres to the pane. Falls back to a native client (vncviewer, xtigervncviewer, Remmina on Linux; TightVNC on Windows). |
| RDP | Embedded in-pane viewer (Apache Guacamole) through a local guacd daemon on Linux. guacd reachability is probed automatically; when it's unavailable β and always on Windows β Ternix hands off to a native client (mstsc, with credentials pre-stashed via cmdkey, or xfreerdp/wlfreerdp). guacd host/port configurable in Settings β Advanced. |
π₯οΈ Terminal
- xterm.js 5.5 with optional WebGL acceleration (auto-disabled when ligatures are on, since ligatures require the DOM renderer)
- Addons: fit, search, web-links (clickable URLs), Unicode 11 width
- Find with match-case and regex toggles, next/previous, and overview-ruler marks
- Context menu: Copy · Paste · Select All · Copy as HTML · Find⦠· Clear · Reset
- Right-click configurable: show the context menu, or paste immediately
- Paste safety: CRLF normalised to LF, optional multiline paste confirmation, optional trailing-whitespace trim
- Copy on select and middle-click paste, both optional
- Terminal bell: off, visual flash, or audio beep
- Font zoom with
Ctrl + / - / 0
- Broadcast mode β type once in a floating bar, send to every pane of every broadcast-enabled tab
πͺ Tabs & Split Panes
- Up to 6 panes per tab, tiled as at most 2 rows Γ 3 columns. Panes share space equally.
- Split right (
Ctrl+Shift+D) or down (Ctrl+Shift+E); a new pane inherits the active pane's session
- Tabs: drag to reorder, middle-click to close, rename,
Ctrl+1β¦Ctrl+9 to jump
- Tab context menu: duplicate tab, rename, split right/down, close tab / others / to the right
- Restore last session on startup, or open a blank tab, or open the session picker
- Auto-reconnect with configurable retries and delay;
Ctrl+R to reconnect a dropped pane manually
- The active pane is outlined when a tab holds more than one
β¨οΈ Command Palette & Keybindings
Ctrl+P opens a fuzzy palette over every saved session plus quick actions (new SSH session, new local shell, split, toggle sidebar, open SFTP, key vault, broadcast, settings, import/export).
All 21 shortcuts below are rebindable in Settings β Keyboard (click a row, press the combo). Overrides are stored as JSON.
| Action | Default | Action | Default |
|---|
| New tab (local shell) | Ctrl+T | Toggle SFTP panel | Ctrl+Shift+F |
| New SSH session | Ctrl+Shift+N | Toggle broadcast | Ctrl+Shift+B |
| Close tab | Ctrl+W | Increase font size | Ctrl+= |
| Next / previous tab | Ctrl+Tab / Ctrl+Shift+Tab | Decrease font size | Ctrl+- |
| Split right | Ctrl+Shift+D | Reset font size | Ctrl+0 |
| Split down | Ctrl+Shift+E | Clear terminal | Ctrl+L |
| Toggle sidebar | Ctrl+B | Disconnect | Ctrl+Shift+Q |
| Command palette | Ctrl+P | Toggle recording | Ctrl+Shift+R |
| Find in terminal | Ctrl+F | Open key vault | Ctrl+Shift+K |
| Open settings | Ctrl+, | Full screen | F11 |
Ctrl+1β¦Ctrl+9 (jump to tab) and Ctrl+R (reconnect) are fixed.
π Sessions
- Unlimited sessions organised into nested group folders; drag a session onto a group to move it
- Fuzzy search across name, host, and tags
- Sort by AβZ, last connected, or protocol
- Duplicate a session in one click
- Metadata: name, protocol, host, port, username, tags, notes, environment variables, startup commands
- Right-click a session: Connect · Edit · Duplicate · Tunnels⦠· Connection log · Copy host · Delete
- Last connected timestamps
π SSH Key Vault
- Private keys encrypted at rest with AES-256-GCM
- Generate ed25519, RSA 4096, or ECDSA 521 keys in-app, with an optional passphrase (
aes256-cbc)
- Import PEM / OpenSSH private keys by file picker or paste
- Scan
~/.ssh to batch-import local keys (passphrase-protected keys are skipped β they can't be fingerprinted unattended)
- Deploy a public key to any saved SSH session β the
ssh-copy-id equivalent
- Copy public key, export private key, delete
SHA256: fingerprints and a "used by N sessions" count
π‘οΈ Host Key Verification
Host keys are pinned in a known_hosts table (SQLite, not the OpenSSH file). Three modes via ssh.hostKeyStrictness:
- prompt (default) β unknown keys raise a dialog showing the fingerprint
- auto-accept β pin silently on first sight
- strict β refuse to connect to an unknown host
A changed host key always raises a warning dialog showing both the stored and the offered fingerprint, in every mode.
π Vault & Security
- Keychain mode (default) β a random 256-bit key is generated on first run and kept in your OS credential store (Windows Credential Manager, Linux Secret Service). The vault is always unlocked.
- Master-password mode β the key is derived with PBKDF2 (100,000 iterations, SHA-512) and never written anywhere. Setting, changing, or removing the master password re-encrypts every stored secret in a single transaction.
- Encrypted at rest: SSH passwords, key passphrases, VNC passwords, and private keys. Blobs are
[IV | GCM tag | ciphertext] with a fresh random IV per encryption.
- Auto-lock on idle (0β1440 min) and on system sleep β master-password mode only
- Clipboard auto-clear after 0β600 s, and only if the clipboard still holds what Ternix copied
> See Security Notes for exactly where the key lives in each mode. It matters.
π SFTP File Manager
- Stacked local and remote panes, each with its own scroller; the whole panel is resizable by a drag handle
- Browse, upload, download, mkdir, rename, delete (recursive on remote directories),
chmod
- Columns for permissions and owner/group, parsed from the server's long listing
- Sort by default (server order), name, or date modified β re-picking the active sort returns to server order
- Toggle hidden files, editable path bar, up/refresh
- Multi-select with
Ctrl/Cmd-click, Shift-click ranges, and Ctrl+A
- Drag and drop between panes β and from your desktop / Explorer / Finder straight onto the remote pane to upload
- Dropping a folder uploads it recursively
- Double-click a remote file to download and open it locally
- Visual
chmod editor β a 3Γ3 permission grid tied to a live octal field
- Name-collision policy: prompt, overwrite, skip, or auto-rename
π Transfers
- Live progress, speed, and ETA per file
- Pause, resume, and cancel any individual file β cancelling one file in a 30-file folder transfer lets the other 29 finish
- Cancelling removes the partially-written file (local for downloads, remote for uploads) so a half-file is never mistaken for a real one
- Configurable parallelism (
transfer.maxConcurrent, default 3), applied across an entire folder tree rather than per-directory
- Optional timestamp preservation
- Transfers move in ~254 KiB chunks β the maximum a single SFTP request allows
π SSH Port Forwarding
- Local (
-L), Remote (-R), and Dynamic (-D, SOCKS5) tunnels, saved per session
- Auto-start on session connect
- Live bytes-transferred counter, status dot, copy address, stop, restart
π Command Snippets
- Reusable commands with name, description, and tags
${VAR} interpolation β each unique variable prompts once at run time
- Multi-line snippets run line by line, each gated on the shell prompt returning, so long commands don't eat the next line as type-ahead
- Global snippets appear everywhere; unchecking "Global" scopes a snippet to the session that owns it
- Fuzzy search; JSON import/export
πΌ Session Recording
- Records terminal output as asciinema v2 (
.cast)
- Built-in player: play/pause, restart, and a scrubbable timeline (idle gaps compressed to 2 s)
- Auto-record every session, optionally
- Storage cap that prunes the oldest recordings; export any
.cast
π System Monitor
A live dashboard for the machine you're connected to β local or remote over SSH β polled every 3 seconds:
- CPU % with a heat-coloured sparkline, load averages, process count, and top process
- Memory and swap
- Per-mount disk usage
- Network RX/TX sparklines (bytes/sec)
- Uptime and temperature sensors
Remote stats are gathered by a small /proc probe over the SSH connection. While a session is still handshaking the panel says so rather than quietly showing your local machine's numbers.
π Connection Log & Global Search
- Every connect/disconnect is logged with host, timestamp, duration, and disconnect reason (last 500)
- Global Search view queries sessions and snippets together
π Import / Export
Import from 7 formats β Ternix JSON backup, OpenSSH config, PuTTY .reg, WinSCP .ini, MobaXterm .mxtsessions, Tabby config.yaml, and generic CSV.
Export to 5 β Ternix JSON, CSV, OpenSSH config, MobaXterm, Tabby. (PuTTY and WinSCP are import-only.)
Smart key auto-linking β IdentityFile and other key paths are resolved against the vault by fingerprint first, then filename. Keys found on disk are imported automatically; keys that can't be located are recorded in the session's notes so nothing silently breaks. Duplicate sessions (same name + protocol + host + user) are skipped.
Exporting a backup with private keys requires the master password, when one is set.
π¨ Themes & Appearance
12 built-in themes: Default Dark Β· Default Light Β· Dracula Β· Nord Β· Solarized Dark Β· Solarized Light Β· Tokyo Night Β· Gruvbox Dark Β· Monokai Β· One Dark Pro Β· Catppuccin Mocha Β· Catppuccin Latte
- Theme builder β edit background, foreground, cursor, selection, UI accent/surface/border, and all 16 ANSI colours, with a live preview. Import/export themes as JSON.
- Font family, size, ligatures, line height, letter spacing
- Cursor style (block/underline/bar) with optional blink
- Custom CSS injection, optional status-bar clock, compact mode
βοΈ Settings
Every setting key and its default
| Section | Key | Default |
|---|
| General | general.defaultShell | (empty) |
| General | general.startupBehavior | blank (blank / reopen / picker) |
| General | general.newTabProtocol | local |
| General | general.confirmCloseActive | true |
| General | general.autoReconnect | false |
| General | general.autoReconnectRetries | 3 |
| General | general.autoReconnectDelay | 3 |
| Terminal | terminal.scrollback | 5000 |
| Terminal | terminal.bell | none (none / visual / audio) |
| Terminal | terminal.wordSeparators | ()[]{}'"` |
| Terminal | terminal.copyOnSelect | false |
| Terminal | terminal.pasteOnMiddleClick | true |
| Terminal | terminal.pasteConfirmMultiline | true |
| Terminal | terminal.trimPasteWhitespace | false |
| Terminal | terminal.rightClick | menu (menu / paste) |
| Appearance | appearance.theme | dark-default |
| Appearance | appearance.fontFamily | JetBrains Mono, Fira Code, β¦ |
| Appearance | appearance.fontSize | 14 |
| Appearance | appearance.ligatures | false |
|
π Auto-Update
Powered by electron-updater against GitHub Releases. Ternix checks on startup (when enabled) and surfaces a toast with a View button that jumps straight to Settings β Updates, where you can download with a live progress bar and restart to install.
The Beta channel opts you into GitHub pre-releases; Stable offers only full releases. The choice takes effect on the next check β no restart needed. Updates only work in packaged builds.
ποΈ Architecture
ternix/
βββ electron/ # Main process (Node.js / Electron)
β βββ main.ts # Window, CSP, power monitor, navigation lockdown
β βββ preload.ts # Context-isolated IPC bridge (TernixApi)
β βββ db/
β β βββ schema.ts # SQLite schema (WAL, foreign keys on)
β β βββ migrations/ # user_version-based forward migrations
β β βββ repo.ts # Repository layer
β β βββ snippetScope.ts # Global vs session-scoped snippet invariant
β βββ ipc/ # 20 IPC namespaces, {ok,data}/{ok,error} envelope
β βββ services/
β βββ SshService.ts # SSH2 client: jump chains, host-key pinning, MFA
β βββ SftpService.ts # SFTP ops + chunked transfers, pause/cancel
β βββ TelnetService.ts # Telnet with IAC option negotiation
β βββ SerialService.ts # serialport (lazy-loaded)
β βββ PtyService.ts # Local PTY (node-pty)
β βββ RdpGatewayService.ts # guacd gateway (guacamole-lite)
β βββ VncBridgeService.ts # Token-gated loopback WebSocketβTCP bridge
β βββ NativeClientService.ts # mstsc / xfreerdp / vncviewer fallbacks
β βββ TunnelService.ts # -L / -R / -D (SOCKS5)
β βββ RecordingService.ts # asciinema v2 + storage cap
β βββ KeyService.ts # Key vault: generate, import, deploy
β βββ CryptoService.ts # AES-256-GCM vault + PBKDF2
β βββ ImportExportService.ts # 7 import / 5 export formats
β βββ ConnectionManager.ts # Active connection registry, OSC 7 cwd tracking
β βββ DatabaseService.ts # SQLite lifecycle
β
βββ src/ # Renderer process (React + TypeScript)
β βββ components/
β β βββ layout/ # RootLayout, ActivityBar, Sidebar, TabBar, StatusBar,
β β β # TitleBar, CommandPalette, Toast, StatsPoller
β β βββ terminal/ # TerminalArea, TerminalPane, SplitLayout, BroadcastBar,
β β β # TerminalSearch, TerminalToolbar, RemoteDesktopPane
β β βββ sidebar/ # SessionTree, GroupFolder, SessionCard, SnippetsPanel,
β β β # TunnelsPanel, RecordingsPanel, StatsPanel,
β β β # SearchPanel, SftpSidebar, TransferQueue
β β βββ sftp/ # SftpPanel, FileList, FileRow, PermissionsEditor
β β βββ settings/ # SettingsPanel + 9 sections
β β βββ dialogs/ # NewSession, KeyVault, Tunnel, ExportImport,
β β β # RecordingPlayer, ThemeEditor, Snippet, ConnectionLog
β β βββ ui/ # Modal, ContextMenu
β βββ store/ # Zustand: tabs, sessions, settings, theme, sftp,
β β # transfers, stats, ui
β βββ hooks/ # useTerminal, useSftp, useKeyboard, useTheme, useSsh
β βββ themes/ # 12 built-in themes
β βββ utils/ # fuzzy, path, sftpSort, statsTarget, snippets, format*
β
βββ electron-builder.json # AppImage + deb (Linux), NSIS + portable (Windows)
βββ electron.vite.config.ts
Key technical decisions
better-sqlite3, synchronous, in the main process β no races on credential storage. WAL journal, foreign keys on, user_version migrations applied in a transaction.
ssh2 for the full protocol: multi-hop jump hosts, the SFTP subsystem, exec channels, and the latency probe.
- SFTP transfers use ~254 KiB chunks (
OPENSSH_MAX_PKT_LEN β PKT_RW_OVERHEAD). ssh2's SFTP streams issue one request and wait for the reply, so on a high-latency link the chunk size is the throughput.
node-pty for local shells with real signal handling; serialport loaded lazily so the app still boots if its native build failed.
- AES-256-GCM with a random IV per encryption; blobs stored as
[IV | tag | ciphertext].
- Zustand for state β the transfer queue subscribes per row, so a progress tick re-renders one row rather than the whole list.
- The renderer never touches Node. Everything goes through one typed
contextBridge surface; each IPC call returns an {ok, data} / {ok, error} envelope and pings the vault idle timer.
- Nothing blocks the main process. System-stats collection is async with TTL caches, because Electron routes your input events through that process.
π Installation
Windows
Download the .exe installer or the portable build from Releases.
Linux
Download the .AppImage or .deb from Releases.
For embedded RDP, install guacd (the .deb declares it as a dependency). Without it, RDP falls back to a native client.
π οΈ Building from Source
Prerequisites
- Node.js 20+ (CI builds on 22)
- Native modules compile via node-gyp, so you need a toolchain:
- Linux:
libsecret-1-dev (keytar), libudev-dev (serialport); rpm and libarchive-tools to package
- Windows: Python with
setuptools (the distutils shim node-gyp needs on Python 3.12+)
Install & run
git clone https://github.com/PraneethReddy-github/ternix.git
cd ternix
npm install
npm run dev
Rebuild native modules
If you hit native module errors (node-pty, better-sqlite3, serialport, keytar):
npm run rebuild
Build for distribution
npm run build:linux # AppImage + .deb β dist-electron/linux
npm run build:win # NSIS + portable β dist-electron/win
π§ͺ Development
npm run typecheck # tsc across renderer + main
npm run lint # eslint over .ts / .tsx
Ternix has no test framework. Instead, non-trivial pure logic keeps a runnable self-check beside it β plain assert, no fixtures. Run them with Node's type stripping:
node --experimental-strip-types electron/db/snippetScope.check.ts
node --experimental-strip-types electron/services/sftpOwner.check.ts
node --experimental-strip-types electron/services/transferOutcome.check.ts
node --experimental-strip-types electron/services/vaultKeyPlan.check.ts
node --experimental-strip-types src/utils/sftpSort.check.ts
node --experimental-strip-types src/utils/statsTarget.check.ts
π Security Notes
Ternix keeps every credential on your machine. Here is precisely how.
What is encrypted
SSH passwords, key passphrases, VNC passwords, and stored private keys are encrypted with AES-256-GCM. Each blob is laid out as [12-byte IV | 16-byte auth tag | ciphertext] with a fresh random IV per encryption, so identical secrets never produce identical ciphertext, and any tampering fails the GCM tag check on decrypt.
Hosts, usernames, ports, notes, tags, and snippet commands are not encrypted β they are ordinary columns in the SQLite database.
Where the vault key lives
Everything above is encrypted with one 256-bit vault key. There are two modes.
Keychain mode (default). The key is generated on first run and stored in your operating system's credential store:
| Platform | Backing store |
|---|
| Windows | Credential Manager |
| Linux | Secret Service (libsecret / GNOME Keyring / KWallet) |
Ternix writes the key, then reads it back and compares before trusting the store. On every launch the key is read from the keychain β it is not kept on disk.
If the machine has no credential store at all (a headless Linux box without libsecret, or a failed keytar build), Ternix falls back to a 0600 key file at /.vaultkey. That fallback is the only situation in which the key touches the filesystem, and in that case the file protects the vault exactly as well as your user account does.
Master-password mode. The key is derived from your password with PBKDF2 (100,000 iterations, SHA-512) against a stored random salt, and is never written anywhere β not the keychain, not the disk. It exists only in memory while the vault is unlocked. Switching modes re-encrypts every stored secret in a single database transaction, and old keys are zeroed in memory afterwards.
Your password itself is never stored. It is verified by decrypting a known verifier string: the wrong password fails the GCM auth tag.
Locking
Master-password mode locks the vault on an idle timeout and on system sleep, zeroing the key. Keychain mode is always unlocked β the key is available whenever the app runs, so the idle and sleep settings do nothing there. Choose master-password mode if you want a vault that actually locks.
Other notes
- Exporting a private key prompts for the master password only when one is set.
- Host keys are pinned on first use; a changed key always raises a warning showing both the stored and the offered fingerprint.
- Embedded RDP does not validate server certificates (
ignore-cert). Treat in-pane RDP as protected by the network path, not by TLS identity.
- The renderer never touches Node:
contextIsolation is on, nodeIntegration is off, webviewTag is off, in-app navigation is blocked, window.open is denied, and a Content-Security-Policy is injected on every response.
- Clipboard entries copied by Ternix can auto-clear after a timeout, and only if the clipboard still holds what Ternix put there.
β οΈ Known Limitations
Documented so you don't discover them the hard way:
- Split panes share space equally; there are no draggable dividers between them.
- Compact mode only hides the per-pane toolbar.
- Per-session terminal encoding is stored but not applied β SSH is UTF-8, Telnet and Serial are byte-transparent.
- X11 forwarding,
server_alive_interval, and RDP colour-depth/fullscreen are exposed in the session editor but not yet wired to the connection.
- Agent forwarding only takes effect when the session's auth type is agent.
- Session, group, and tab accent colours exist in the data model but no UI sets them yet.
- Accepting a changed host key does not overwrite the stored pin, so you'll be asked again next connect.
- Embedded RDP is Linux only; Windows always uses
mstsc.
~/.ssh scanning skips passphrase-protected keys (they can't be fingerprinted without the passphrase).
- macOS is not supported. It has never been tested, nothing is built or released for it, and no macOS artifacts exist.
π¦ Core Dependencies
| Package | Purpose |
|---|
@xterm/xterm + addons (fit, search, web-links, webgl, unicode11) | Terminal emulator |
ssh2 | SSH2 protocol client + SFTP |
node-pty | Local shell PTY |
serialport | Serial / COM |
better-sqlite3 | Synchronous SQLite |
keytar | OS keychain (optional) |
guacamole-lite + guacamole-common-js | Embedded RDP via guacd |
@novnc/novnc + ws | Embedded VNC over a loopback bridge |
js-yaml | Tabby YAML import/export |
react + react-dom + zustand | UI and state |
lucide-react | Icons |
electron-updater (optional) | Auto-update against GitHub Releases |
π€ Contributing
Contributions are welcome β please open an issue first to discuss substantial changes.
- Fork the repository
- Create your branch (
git checkout -b feature/amazing-feature)
- Run
npm run typecheck and the relevant *.check.ts self-checks
- Commit (
git commit -m 'feat: add amazing feature')
- Open a Pull Request
π License
MIT β see LICENSE.
Built with β€οΈ using Electron, React, and xterm.js
>T<