A Go-based SSH terminal emulator with session management, built on Fyne 2 GUI framework and the gopyte terminal emulation library.
The first fully-functional Fyne-based SSH terminal with session management, scrollback history, bracketed paste, and text selection.
Hierarchical folder/session tree with search filter, connection status indicators, and multi-tab interface
Light application chrome around a dark terminal pane, scrolled into history with the draggable scrollbar on the right edge - an example of the independent app/terminal theming
Click-drag selection with the right-click Copy/Paste context menu
htop with full color support (256-color and 24-bit truecolor), proper resize handling, and alternate screen buffer (Cyber theme)
btop rendering at full fidelity: 256-color and truecolor, Braille sub-cell meters and graphs, and the dense, high-frequency alternate-screen redraw of a full system dashboard. btop's heavy use of wide and combining characters was the workload that surfaced - and then validated the fix for - a stubborn wide-character handling bug in the gopyte engine. Clean btop rendering is a project milestone: it exercises nearly every part of the terminal emulation path at once.
Corporate chrome around a dark terminal pane running vim - the chrome and terminal are themed on separate axes
CRUD interface for managing sessions and folders with full authentication configuration
AES-256-GCM credential store unlocked by an Argon2id master password; add password or SSH-key credentials and reference them from sessions and Quick Connect
Ad-hoc connections without saving a session
Per-theme color overrides on the Colors tab, with live preview
- Line editing no longer corrupts the prompt. Backspace (
BS,0x08) is now a non-destructive cursor-left move, as a VT terminal requires. Previously it cleared the cell it landed on, so any leftward movement the shell emits as a run of backspaces - the left arrow, Ctrl-A to start of line, and mid-line insert/delete - erased or scrambled characters. - Ctrl-A / Ctrl-Z / Ctrl-Y reach the remote shell on Linux and Windows. On those platforms the GUI toolkit captures these as Select-All / Undo / Redo before they become control bytes, so they were silently dropped. They are now forwarded to the PTY (
0x01/0x1A/0x19), restoring readline beginning-of-line, job suspend, and yank. (macOS was unaffected, as it binds those actions to Cmd.) - Close Other Tabs / Close All Tabs in the terminal right-click menu, with a single confirmation covering the whole batch instead of one prompt per connected session.
- Bind a vault credential when editing a session. The Edit Session dialog now has a Vault Credential picker; selecting one stores only a reference (
credsid) and leaves the secret in the encrypted vault, resolved at connect time. A working Password field and correct auth-type toggling were also restored to that dialog (it previously had no password field and could wipe stored auth on save). - Help menu with About (product name, version, and project link).
- Session tree starts collapsed instead of expanding every folder on launch.
- SSH connectivity with password and public key authentication
- Multi-tab terminal interface with tree-based session navigator
- Full terminal emulation via gopyte (VT100/ANSI parsing)
- Keyboard input routing to SSH sessions
- Dynamic terminal resize with proper SSH window-change signaling
- Full-screen applications (htop, btop, vim, nano, etc.) via the alternate screen buffer
- Full color support - 16-color, bright/AIXTERM (90-97/100-107), 256-color (6x6x6 cube and grayscale ramp), and 24-bit truecolor; all mapped through the active terminal theme, so the low 16 follow the theme palette while the cube, grayscale, and truecolor render their exact values
- Correct UTF-8 rendering for multibyte glyphs, including those whose bytes coincide with 8-bit C1 control codes - the braille glyphs btop draws its graphs with are the common case; in UTF-8 mode high bytes (0x80-0x9F) are treated as text rather than control introducers, and multibyte sequences split across network reads are reassembled instead of corrupted
- Independent app and terminal theming - three built-in themes (Cyber, Light, Corporate) selectable separately for the chrome and the terminal pane, with per-theme color customization
- Flicker-free standard-mode rendering via incremental per-cell updates
- Adjustable per-tab terminal font size (Settings -> Font Size, 8-28 pt) for accessibility, applied through a per-terminal theme override; hit-testing, text selection, and the PTY row/column count are all measured from the rendered cell, so they track the chosen size
- Text selection and SGR cell backgrounds rendered directly on the terminal grid, so the selection highlight is visible in both standard and full-screen (vim/htop) modes
- Scrollback history with mouse wheel scrolling and a draggable scrollbar; buffer size is configurable (Settings -> Scrollback Lines)
- Text selection with clipboard support (double-click word, triple-click line), including click-drag selection that spans both scrollback history and the live view, auto-scrolling when the pointer reaches an edge
- Right-click Copy/Paste context menu in the terminal
- Bracketed paste - multi-line content (configs, scripts) pastes verbatim into vim, editors, and shells, with no auto-indent cascade or premature line-by-line execution
- Optional paced paste with a configurable per-line delay for slow CLI parsers (network gear, serial-style links)
- Tree-based session navigator with collapsible folders
- Full session/folder management from the tree itself - right-click a session for Connect, Edit, Duplicate, Move to Folder, Move Up/Down, and Delete; right-click a folder for New Session Here, New Folder, Rename, reorder, and Delete. The whole row is the hit target, not just the label.
- Window menu bar (File / Edit / Tools) for all actions - native top-of-screen bar on macOS, in-window bar on Windows/Linux - replacing the old toolbar button row
- Import and export sessions to/from a YAML file via a native file picker, with merge-or-replace and duplicate skipping; the on-disk format is the TerminalTelemetry schema, so session files round-trip between the two
- Session persistence via YAML configuration, with stable per-session identity so reordering, moving, and renaming never disturb open tabs
- Session search/filter for quick access to devices
- Session editor with full CRUD operations
- Quick Connect dialog for ad-hoc connections
- SSH key authentication with encrypted key support
- Settings dialog with persistent configuration
- Session logging - cleaned, timestamped transcript per session, toggled live from the terminal right-click menu (Start/Stop Logging), with optional auto-start per session or globally
- Encrypted credential vault - AES-256-GCM store unlocked by an Argon2id-derived master password, with saved credentials referenced from sessions (Creds ID) and Quick Connect
- Vault credential binding from the Session Editor - pick a saved credential while editing a session; only the reference is stored, the secret stays in the vault
- Tab management - Close Other Tabs and Close All Tabs from the terminal right-click menu, with a single batch confirmation
- Control-key passthrough to the remote shell on all platforms, including Ctrl-A / Ctrl-Z / Ctrl-Y, which the GUI toolkit otherwise reserves on Linux/Windows
- Help menu with an About dialog (version and project link)
- SSH agent support not fully implemented
- Host key verification not yet implemented
- Alternate-screen (full-screen app) rendering uses a full-grid repaint rather than the incremental path used in standard mode
- At fractional display scaling (e.g. 1.5x/2x), adjacent selection/SGR cell backgrounds may show faint seams - selection and backgrounds are currently painted as TextGrid cell backgrounds; the seam-free background overlay is bypassed because it does not composite under the per-terminal font-size theme override
TetherSSH uses gopyte, a custom terminal emulation library written specifically for this project. Unlike the Fyne project's proof-of-concept terminal, gopyte provides:
- Full history buffer with configurable scrollback (default 1000 lines)
- Wide character support for CJK characters and emojis, with UTF-8 high bytes correctly treated as text rather than 8-bit C1 controls (so glyphs whose bytes overlap C1 codes, such as btop's braille, render intact)
- Alternate screen buffer for full-screen applications (vim, htop, btop, less)
- Proper resize handling that syncs local screen, gopyte buffers, and SSH session
Standard-mode output is rendered incrementally: each redraw diffs against the on-screen grid and repaints only the cells that actually changed, so typing stays flicker-free even over colored prompts. All redraws are driven by a single coalesced update pass on a short timer rather than one repaint per byte received. Full-screen (alternate-screen) applications use a full-grid repaint, since they tend to redraw most of the screen on each update anyway.
- Bracketed paste (DEC mode 2004): when the remote application requests it (vim, shells, editors), pasted text is wrapped in paste markers so it inserts verbatim - no auto-indent stair-stepping, and no multi-line commands running line by line. Mode 2004 is tracked on both the local PTY and SSH output paths.
- Optional line pacing: a configurable per-line delay (Settings -> Paste Line Delay) for slow CLI parsers on network gear or serial-style links that do not support bracketed paste.
- Unified paste path: the Ctrl+V / Cmd+V shortcut and the right-click Paste menu both route through the same handler, and Ctrl+C aborts an in-flight paced paste.
- Configurable scrollback buffer (Settings -> Scrollback Lines), applied to newly connected sessions.
- Navigate history with the mouse wheel or the draggable scrollbar on the terminal's right edge. The thumb size reflects how much of the buffer is currently on screen, and dragging it scrubs proportionally through the scrollback; it reaches the very top and very bottom of history.
- Click-drag selection works across both scrollback history and the live view - start a selection up in history and drag toward the bottom edge and it auto-scrolls to keep extending. Double-click selects a word, triple-click a line.
- The selection highlight is drawn on the terminal grid cells, so it shows in full-screen apps (vim, htop, less) as well as the standard shell view.
- Copy via the right-click menu or the clipboard shortcut. A settled selection is tied to the view it was made in: scrolling the wheel, typing, or jumping back to the prompt clears it, so the highlight never strands itself against moved text.
- Tree-based navigator with collapsible folders and session counts. The tree starts collapsed; folders expand on click (and auto-expand when you connect or add a session into one).
- Close tabs from the terminal right-click menu - Close Other Tabs and Close All Tabs, each with a single confirmation for the whole batch (the per-tab close button is unchanged).
- Manage everything from the tree by right-click: on a session, Connect / Edit / Duplicate / Move to Folder / Move Up / Move Down / Delete; on a folder, New Session Here / New Folder / Rename / Move Up / Move Down / Delete. The entire row is right-clickable, and the actions also live in the File menu.
- Reorder, move, and rename freely - each session carries a stable internal identity, so changing its position or folder never breaks the tab it is connected in. Manual folder order is preserved (folders are no longer force-sorted alphabetically).
- Real-time search/filter - instantly find sessions by name, host, group, or device type
- Multiple concurrent SSH connections in tabs
- Visual connection status indicators
- One-click connect with automatic credential handling
- Optional compact tree - hide the row icons (Settings -> Appearance) to fit more sessions on screen
- Export all sessions to a YAML file from a native save dialog (File -> Export Sessions). The file uses the same schema as TerminalTelemetry, so it imports cleanly there and into other TetherSSH installs.
- Import from a YAML file via a native open dialog (File -> Import Sessions), choosing Merge (add to the current set, skipping sessions whose name + host + port already exist in the target folder) or Replace (the file becomes the whole set). Import preserves every field, including device metadata.
- Password authentication - prompted on connect or stored in session
- SSH key authentication - supports RSA, ECDSA, Ed25519 keys
- Encrypted key support - passphrase prompts for protected keys
- Keyboard-interactive - for MFA/RADIUS environments (including YubiKey)
- Configurable per-session authentication type
- Saved credentials - reference a vault entry from a session or Quick Connect instead of storing secrets per session (see Credential Vault)
An encrypted, password-protected store for reusable SSH credentials, so passwords and key passphrases live in one place instead of being copied across session definitions. Open it from Tools -> Credential Vault.
- Encryption at rest - credentials are sealed with AES-256-GCM in a single file,
~/.tetherssh/credentials.vault(written0600). The plaintext is a JSON list of credentials; on disk it is one authenticated ciphertext blob. - Master password - the AES-256 key is derived from your master password with Argon2id (64 MiB, 3 iterations, 4 lanes) using a per-file random salt, and is held in memory only while the vault is unlocked. The master password itself is never written to disk, and a wrong password surfaces as a GCM authentication failure rather than a comparison against stored text. Minimum master-password length is 8.
- Lock / unlock - unlock once to use saved credentials; Lock clears the in-memory key immediately. You can change the master password from the vault UI, which re-encrypts the store under the new key. The KDF parameters and salt are stored in the vault file, so an existing vault keeps opening even if the defaults change in a later version.
- Per-credential fields - name, username, auth type (password or SSH key), password or key path + passphrase, an optional default flag, and last-used tracking.
- Use in sessions - a session can carry a Creds ID (YAML
credsid) referencing a vault entry by name or ID. On connect, the referenced credential fills in username and auth details, but only fields the session leaves blank, so per-session values still take precedence. If the vault is locked, the reference is skipped. - Use in Quick Connect - the Quick Connect dialog has a "Use saved credential" selector that pre-fills the connection fields from the vault.
Security note: the vault protects credentials at rest behind your master password. While it is unlocked, the derived key and decrypted secrets are held in process memory, as any client must to use them.
Open File -> Quick Connect for ad-hoc connections without saving a session:
- Enter host, port, username
- Choose Password or SSH Key authentication
- Default key path: ~/.ssh/id_rsa
- Optional key passphrase for encrypted keys
Open Tools -> Session Editor for the full session manager:
- Folders panel - organize sessions into groups
- Sessions panel - view/edit sessions in selected folder
- Add/Edit/Delete - full CRUD operations
- Device metadata fields (type, vendor, model) for network equipment
- Bind a vault credential - the Edit Session dialog has a Vault Credential picker and a read-only indicator of the bound credential. Choosing one stores only the reference (
credsid); the password or key passphrase stays in the encrypted vault and is resolved on connect. A per-session Password field is available too, though for persistence the vault binding is the path that survives a save (the session file never stores a plaintext password).
For importing and exporting session files, see Import / Export above; both the editor and the File menu route through the same file-picker flow.
All configuration is stored in ~/.tetherssh/:
- Sessions:
~/.tetherssh/sessions/sessions.yaml - Settings:
~/.tetherssh/settings.json - Credential vault:
~/.tetherssh/credentials.vault(encrypted; see Credential Vault) - Logs:
~/.tetherssh/logs/(when logging is enabled)
# TetherSSH Sessions File
# Auth types: password, publickey, keyboard-interactive
- folder_name: Production
sessions:
- display_name: web-server-01
host: 10.0.1.10
port: "22"
username: admin
auth_type: publickey
key_path: ~/.ssh/id_rsa
DeviceType: linux
- display_name: edge-switch-01
host: 10.0.1.20
port: "22"
credsid: net-admin # pull username/auth from the vault entry "net-admin"
DeviceType: arista_eos
- folder_name: Lab
sessions:
- display_name: cisco-router
host: 172.16.1.1
port: "22"
username: admin
auth_type: password
DeviceType: cisco_ios
Vendor: Ciscocredsid references a Credential Vault entry by name or ID; on connect it supplies username and auth details for any fields the session leaves blank (requires the vault to be unlocked).
Open Tools -> Settings to open the Settings dialog. Settings are persisted to ~/.tetherssh/settings.json.
| Setting | Description | Default |
|---|---|---|
| Row Offset | Adjust terminal row calculation (increase for Retina/HiDPI displays) | 2 |
| Column Offset | Adjust terminal column calculation | 0 |
| Font Size | Terminal font size in points (range 8-28), applied per session/tab via a scoped theme override; the grid, hit-testing, text selection, and the PTY row/column count all follow the rendered cell. Applies to newly connected tabs | 12 |
| Scrollback Lines | History buffer size; takes effect on newly connected sessions | 1000 |
| Paste Line Delay | Per-line delay for multi-line paste; paces output for slow CLI parsers (None / 25 / 50 / 100 / 250 ms) | None |
| Copy on Select | Auto-copy selected text to clipboard (not yet implemented) | Off |
| Setting | Description | Default |
|---|---|---|
| App Theme | Theme for the application chrome (sidebar, tabs, toolbar, dialogs) | Cyber |
| Terminal Theme | Theme for the terminal pane (background, default text, ANSI palette) | Cyber |
| Remember Window Size | Restore window dimensions on startup | On |
| Hide icons in the session tree | Drop the folder/session row icons for a more compact tree | Off |
The two selectors are independent - see Themes below. Changing the App Theme applies immediately; the Terminal Theme applies to newly connected tabs (reconnect an open tab to repaint it).
TetherSSH separates theming into two independent axes, both set on the Appearance tab:
- App Theme drives the application chrome - sidebar, tabs, toolbar, dialogs, buttons - via the Fyne theme.
- Terminal Theme drives the terminal pane on its own - its background, the default (unset-color) text color, and which ANSI palette colored output maps to.
Because the axes are separate, the chrome and the terminal can run different themes. Three built-in themes are available on each axis:
| Theme | Chrome | Terminal pane |
|---|---|---|
| Cyber | Deep blue-black with cyan / matrix-green accents | Black background, bright on-dark ANSI palette |
| Light | White surfaces with a Microsoft-blue accent (Fluent/Office) | White background, dark-on-light ANSI palette |
| Corporate | Navy with cyan accents | Navy background, bright on-dark ANSI palette |
This yields any combination of the two - for example Corporate chrome over a Light terminal gives a navy application frame around a white terminal pane.
The terminal's default text color (output with no explicit color, e.g. most shell and htop text) is taken from the active terminal palette's default entry, so it stays legible against that palette's own background rather than inheriting the chrome's foreground.
Each built-in theme can be customized. The Colors tab has a sub-tab per theme (Cyber, Light, Corporate); leave a field blank to use the theme's built-in value. Overridable colors include primary/accent, secondary, background, surface, surface variant, foreground, input background/border, selection, hover, and error/success/warning. A live preview updates as you edit, and changes apply without restarting. Overrides are stored per theme in settings.json.
| Setting | Description | Default |
|---|---|---|
| Default SSH Key | Default key path for new sessions | ~/.ssh/id_rsa |
| Default Port | Default SSH port | 22 |
| Default Username | Pre-fill username for new sessions | (empty) |
| Connection Timeout | SSH connection timeout in seconds | 30 |
| Keepalive Interval | SSH keepalive interval in seconds | 60 |
Session logs are cleaned transcripts: ANSI/control sequences are stripped and carriage-return/backspace edits applied, so each logged line matches what was on screen. Logging is toggled live per session from the terminal right-click menu (Start/Stop Logging); the settings below are the global defaults.
| Setting | Description | Default |
|---|---|---|
| Auto-start logging on new connections | Begin logging automatically when a session connects | Off |
| Log Directory | Directory for session logs | ~/.tetherssh/logs |
| Add timestamps to log entries | Prefix each line with a timestamp | On |
Log filename format: {session_name}_{YYYYMMDD_HHMMSS}.log
Any session may also set logging: true in its YAML to auto-start logging on connect. Full-screen apps (vim, htop) drive the screen by cursor addressing and will log as noise - logging is intended for line-oriented CLI sessions.
tetherssh/
├── cli/
│ ├── main.go # Application entry, window setup
│ ├── debug.go # Trace gating (TETHERSSH_DEBUG app-level / TETHERSSH_TRACE parser)
│ ├── paths.go # Centralized path management (~/.tetherssh)
│ ├── ssh_manager.go # SessionManager - tree navigator, search, tabs, main menu
│ ├── session_persistence.go # YAML load/save, SessionStore, import/export
│ ├── session_tree_ops.go # Tree right-click ops: folder/session CRUD, reorder, move, import/export UI
│ ├── session_editor.go # CRUD modal dialog
│ ├── settings.go # Application settings dialog
│ ├── ssh_backend.go # SSH client, auth chain, SSHTerminalWidget
│ ├── credential_vault.go # Encrypted credential store (AES-256-GCM, Argon2id)
│ ├── credential_vault_dialog.go # Credential vault UI (add/edit, lock/unlock)
│ ├── terminal_logger.go # Session logging - cleaned, timestamped transcript
│ ├── terminal_widget.go # NativeTerminalWidget - core terminal UI
│ ├── terminal_pty.go # Local PTY support, WriteToPTY, history
│ ├── terminal_events.go # Keyboard/mouse event handling
│ ├── terminal_events_bus.go # Event bus for terminal events
│ ├── terminal_display.go # TextGrid rendering, incremental cell diff, viewport
│ ├── terminal_bglayer.go # Cell/selection background overlay (HiDPI-safe snapped runs; currently bypassed - see Known Issues)
│ ├── terminal_paste.go # Paste handling - bracketed paste, line pacing, context menu
│ ├── terminal_selection.go # Text selection and clipboard
│ ├── terminal_scrollbar.go # Draggable scrollbar for virtual scrollback
│ ├── terminal_containers.go # Custom container widgets
│ ├── tappable_tree_node.go # Right-click support for tree nodes
│ ├── theme.go # Named themes (chrome + terminal), color mappings
│ ├── pty_unix.go # Unix PTY implementation
│ └── pty_windows.go # Windows PTY implementation
├── internal/
│ └── gopyte/ # Terminal emulation library
│ ├── screen.go # Base screen buffer (NativeScreen)
│ ├── debug.go # Parser trace gating (TETHERSSH_TRACE)
│ ├── screen_interface.go # Screen interface definitions
│ ├── history_screen.go # Scrollback history management
│ ├── wide_char_screen.go # Wide character support
│ ├── alternative_screen.go # Alternate screen buffer (vim, htop)
│ ├── streams.go # ANSI escape sequence parser
│ ├── escape.go # Escape sequence definitions
│ ├── control.go # Control character handling
│ ├── graphics.go # SGR/graphics attributes
│ ├── modes.go # Terminal mode handling
│ ├── charset.go # Character set support
│ └── gopyte_test/ # Test suite
├── screenshots/ # Documentation images
│ └── v0.3/ # Current release screenshots + overview gif
├── build-linux.sh # Native Linux build -> dist/linux/
├── build-macos.sh # Native macOS build (arm64/amd64/universal) -> dist/macos/
├── build-windows.ps1 # Native Windows build (-H windowsgui) -> dist/windows/
├── go.mod
├── go.sum
├── LICENSE
└── README.md
- Go 1.24 or later (matches the
godirective ingo.mod) - GCC compiler (required for CGO/OpenGL):
- Windows: Install TDM-GCC from https://jmeubank.github.io/tdm-gcc/ (MinGW-w64 based)
- Linux:
sudo apt install build-essential(Debian/Ubuntu) or equivalent - macOS:
xcode-select --install
The pty_unix.go / pty_windows.go split is selected automatically by GOOS via //go:build constraints, so no -tags flag is needed.
# Run directly
go run ./cli
# Build with debug symbols
go build -o tetherssh ./cli # Linux/macOS
go build -o tetherssh.exe ./cli # WindowsBuilds are quiet by default. Set TETHERSSH_DEBUG=1 to enable app-level per-frame trace logging (render loop, redraw timing, scroll/selection) for diagnostics:
TETHERSSH_DEBUG=1 go run ./cliThe gopyte parser's per-escape-sequence trace is gated separately behind TETHERSSH_TRACE=1, since it is high-volume (hundreds of lines per full-screen frame) and would otherwise drown out and skew the app-level timing. Enable it only when debugging the parser itself.
Stripped binaries are approximately 50% smaller with no debug symbols. On Windows, -H windowsgui suppresses the background console window for the GUI app.
# Linux/macOS
go build -trimpath -ldflags="-s -w" -o tetherssh ./cli
# Windows
go build -trimpath -ldflags="-s -w -H windowsgui" -o tetherssh.exe ./cliConvenience scripts at the repo root build a release binary into dist/<os>/. Run each on its target OS:
./build-linux.sh # -> dist/linux/tetherssh
./build-macos.sh # -> dist/macos/tetherssh (host arch)
./build-macos.sh universal # arm64 + amd64 fat binary via lipo.\build-windows.ps1 # -> dist\windows\tetherssh.exe (GUI, no console)
.\build-windows.ps1 -Console # keep the console window for stdout/logsEach sets CGO_ENABLED=1 (required by Fyne) and -trimpath -ldflags "-s -w"; pass STRIP=0 (or -Strip:$false on Windows) to keep symbols for debugging.
Cross-compiling Fyne apps requires CGO, which complicates cross-builds. Build natively on each target platform (see the build scripts above), or cross-compile as follows.
Linux -> Windows is straightforward with the mingw-w64 cross toolchain:
sudo apt install -y gcc-mingw-w64-x86-64
CGO_ENABLED=1 GOOS=windows GOARCH=amd64 CC=x86_64-w64-mingw32-gcc \
go build -trimpath -ldflags "-s -w -H windowsgui" -o dist/windows/tetherssh.exe ./cliCross-compiling to macOS from Linux additionally needs the macOS SDK (via osxcross), so a real Mac or CI is usually simpler. For a one-machine path to both, use fyne-cross with Docker:
# Install fyne-cross
go install github.com/fyne-io/fyne-cross@latest
# Build for Windows from Linux/macOS
fyne-cross windows -arch=amd64 ./cli
# Build for Linux from macOS/Windows
fyne-cross linux -arch=amd64 ./cli| Build Type | Size |
|---|---|
| Debug (full symbols) | ~53 MB |
| Release (stripped) | ~26 MB |
| Zipped release | ~12 MB |
fyne.io/fyne/v2 # GUI framework
golang.org/x/crypto/ssh # SSH client
golang.org/x/crypto/argon2 # Credential vault master-password KDF
github.com/creack/pty # Local PTY (Unix)
github.com/mattn/go-runewidth # Wide character width calculation
github.com/google/uuid # Tab/session unique IDs
gopkg.in/yaml.v3 # Session persistence
TetherSSH stores all configuration in ~/.tetherssh/:
| File | Purpose |
|---|---|
~/.tetherssh/sessions/sessions.yaml |
Session definitions |
~/.tetherssh/settings.json |
Application settings |
~/.tetherssh/credentials.vault |
Encrypted credential store (AES-256-GCM) |
~/.tetherssh/logs/ |
Session logs (when enabled) |
On first run, a stub sessions file is created with example entries.
| YAML Value | Description |
|---|---|
password |
Prompt for password on connect |
publickey |
Use SSH key from key_path |
keyboard-interactive |
MFA/RADIUS environments |
The ~ character is expanded to the user's home directory:
~/.ssh/id_rsabecomes/home/username/.ssh/id_rsa(Linux)~/.ssh/id_rsabecomes/Users/username/.ssh/id_rsa(macOS)~/.ssh/id_rsabecomesC:\Users\username\.ssh\id_rsa(Windows)
- Fix vim/htop resize issues
- Implement resize callback pattern
- Post-connect resize sync
- Buffer bounds safety in gopyte
- Session persistence (YAML config)
- Public key authentication
- Encrypted key passphrase support
- Session search/filter
- Session editor with CRUD
- Quick Connect dialog
- Tree-based session navigator
- Settings dialog with persistence
- Right-click context menu
- Bracketed paste support (DEC mode 2004)
- Right-click Copy/Paste in terminal
- Optional paced multi-line paste
- Incremental, flicker-free standard-mode rendering
- Full color support: 16-color, bright/AIXTERM, 256-color (cube + grayscale), and 24-bit truecolor, mapped through the active terminal theme
- Correct UTF-8 rendering for multibyte glyphs whose bytes overlap 8-bit C1 controls (fixes corruption in btop and similar TUIs), plus reassembly of sequences split across network reads
- HiDPI-safe background and selection rendering (no tiling seams)
- Session logging (cleaned, timestamped transcripts; live right-click toggle)
- Configurable scrollback buffer size
- Draggable scrollbar for scrollback navigation (reaches full top/bottom)
- Text selection across scrollback history (auto-scroll while dragging)
- Verbose trace logging gated behind debug flags (TETHERSSH_DEBUG app-level, TETHERSSH_TRACE parser)
- Encrypted credential vault (AES-256-GCM, Argon2id master password) with lock/unlock and master-password change/re-key
- Saved-credential references from sessions (Creds ID) and Quick Connect
- Full folder/session management from the tree (create, rename, delete, duplicate, reorder, move between folders) with stable per-session identity
- Window menu bar (File / Edit / Tools) replacing the toolbar button row
- Native file-picker import/export with merge-or-replace and duplicate skipping (TerminalTelemetry-compatible YAML)
- Compact UI option (hide tree icons) and denser default chrome
- Selection and PTY sizing driven by the terminal's measured cell, independent of chrome text size
- Adjustable per-tab terminal font size (accessibility), with selection visible in both standard and full-screen modes
- Native build scripts for Linux, macOS, and Windows
- Non-destructive backspace and full control-key passthrough (Ctrl-A/Z/Y reach the shell on all platforms)
- Close Other Tabs / Close All Tabs from the terminal right-click menu
- Vault credential binding from the Session Editor (reference-only, secret stays in the vault)
- Help / About menu
- Implement SSH agent support
- Add host key verification with known_hosts
- Bring incremental rendering to the alternate screen (full-screen apps)
- More cross-platform testing (Windows, Linux, macOS)
- Log viewer/browser
- Optional auto-lock after inactivity
- Split panes (horizontal/vertical)
- Find in terminal output
- Clickable URLs
- Command snippets/macros
- SFTP file browser integration
- Port forwarding UI
- Jump host/proxy support
- Session import from PuTTY/SecureCRT
gopyte is a terminal emulation library built specifically for TetherSSH. It provides:
- NativeScreen: Base screen buffer
- HistoryScreen: Adds scrollback history
- WideCharScreen: Adds wide character support and alternate screen buffer
- VT100/ANSI parsing via Stream.Feed()
- Scrollback history with linked list storage
- Alternate screen buffer for vim, htop, less, etc.
- Wide character support for CJK and emojis
- Resize handling that preserves content and history
- Cursor movement (CUP, CUU, CUD, CUF, CUB)
- Erase operations (ED, EL)
- SGR attributes - bold, italic, underline, reverse, strikethrough
- Color - 16-color (30-37/40-47), bright/AIXTERM (90-97/100-107), 256-color (38;5 / 48;5, cube + grayscale), and 24-bit truecolor (38;2 / 48;2)
- UTF-8 text input, with high bytes (0x80-0x9F) handled as multibyte text rather than 8-bit C1 control introducers
- Scroll regions (DECSTBM)
- Alternate screen (DECSET/DECRST 1049)
- Bracketed paste mode (DECSET/DECRST 2004)
- Window title (OSC 0, 1, 2)
- Secure Cartography: https://github.com/scottpeterman/secure_cartography - Network discovery and topology mapping
- TerminalTelemetry: https://pypi.org/project/terminaltelemetry/ - PyQt6-based SSH terminal with real-time monitoring
- VelociTerm: https://github.com/scottpeterman/velociterm - Web-based SSH terminal with NetBox integration
Status: known, unscheduled. Captured here while it is fresh. Not yet on the roadmap.
The terminal has two parallel render paths that both mutate the same TextGrid and viewport state. Only one is fully maintained; the other is a legacy path that several event handlers still call directly. Because the two have diverged, fixes made to one do not reach the other, and any frame painted by the legacy path leaves dependent UI state - most visibly the scrollbar - stale.
Unified path (maintained).
- Entry:
performRedrawUnified()interminal_widget.go. - Helpers:
renderNormalModeUnified,renderAlternateScreenUnified,calculateUnifiedViewport,extractUnifiedVisibleContent/...Attributes,adjustUnifiedCursor,updateUnifiedScrollBar. - Driven by
updateProcessor()- a ~30 FPS ticker started at construction - with an in-flightredrawingguard that coalesces repaints to the latest state. - Updates everything the UI depends on: viewport, cell diff, cursor, selection overlay, and the scrollback scrollbar.
Direct / legacy path (partially superseded).
- Entry:
performRedrawDirect()interminal_display.go. - Helpers:
renderNormalMode,renderAlternateScreen,calculateVirtualViewport,extractVisibleContent/...Attributes,adjustCursorForViewport. - Reached from the Backspace handler in
terminal_events.goand fromtriggerImmediateRedraw()(called from two sites interminal_widget.go). - Does not call
updateUnifiedScrollBar- it repaints the grid but leaves the scrollbar untouched.
A third loop, enhancedUpdateProcessor() (a 16 ms / ~60 FPS ticker in terminal_events.go), also targets the direct path but is dead code: it is defined and never started with a go statement. It is a maintenance trap because it reads like the live render loop while contributing nothing.
The unified path was introduced to consolidate rendering behind one viewport / scrollbar / selection implementation. The direct path is the older renderer; it was largely superseded but never removed, and a few handlers still call it for immediate feedback on specific keys to avoid waiting up to a ticker interval. The SSH data path was already migrated off it - see the comment in ssh_backend.go noting that calling performRedrawDirect on every chunk made two render passes compete over the same TextGrid, which was the original flicker source.
- Scrollbar desync. A frame painted by the direct path does not update the thumb. On Backspace, the grid repaints via the direct path and the scrollbar position/size only catches up on the next unified tick. (This also made the scrollbar work hard to land - the two paths had to be reasoned about separately.)
- Competing repaints over one TextGrid. Both paths resize and rewrite the same grid via
setStyledRows. Historically this caused flicker; one such double-call was already removed from the SSH path. - Debugging confusion. Logs interleave
NORMAL (linux): ...(unified) withperformRedrawDirect: ... NORMAL: ...(direct), which reads like a single loop. EditingrenderNormalMode(direct) when the live frame came fromrenderNormalModeUnified(unified), or vice versa, is an easy and expensive mistake. - Silent drift. Features and fixes added to one path (scrollbar wiring, selection alignment, alt-screen handling) do not automatically apply to the other.
The goal is a single source of truth for viewport calculation, content extraction, cursor adjustment, scrollbar update, and selection overlay. A low-risk, incremental route:
- Point the immediate-redraw call sites (the Backspace handler and
triggerImmediateRedraw) at a single unified "render now" entry point that runs the same code as the ticker but skips the coalescing delay. This keeps the input responsiveness those sites were added for while removing the divergence. (Mind the threading: the direct call sites already wrap infyne.Do, andperformRedrawUnifiedmarshals internally, so the consolidation needs a path that does not double-marshal.) - Verify the cases the direct path was kept for: Backspace echo latency, and alternate-screen apps (htop, vim), where the two paths handle the alternate buffer differently.
- Delete the now-unreferenced legacy helpers in
terminal_display.goand the deadenhancedUpdateProcessor.
Rough order: one to two focused days for the consolidation and deletion, plus a comparable amount for testing - conditional on the two viewport calculations turning out to be behaviorally equivalent. The risk, and the reason this is not a trivial delete, is that calculateVirtualViewport (direct) and calculateUnifiedViewport (unified) are independent implementations of the same idea and may differ at the edges: history-paging boundaries, alternate-screen entry/exit, and cursor clamping. The real cost is proving equivalence without regressing input latency or full-screen-app rendering, both of which are easy to break and slow to notice. If the calculations are not equivalent, the effort grows with each behavioral difference that has to be reconciled.
A near-free partial mitigation, independent of the full consolidation: have the direct path also update the scrollbar (call updateUnifiedScrollBar with its computed viewport) so the thumb stops desyncing on Backspace. This does not touch the threading or timing model and can ship ahead of the larger cleanup.
MIT License - See LICENSE file
Last updated: May 31, 2026
Author: Scott Peterman










