Skip to content

scottpeterman/tetherssh

Repository files navigation

TetherSSH - SSH, Telnet & Serial Terminal Emulator

A Go-based SSH, telnet, and serial 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 - now with telnet and local serial console support, so the same terminal reaches remote hosts over SSH, telnet-only and console-server gear over the network, and directly-attached gear over a console cable.

View on Fyne Apps Showcase

TetherSSH overview

Screenshots

About

TetherSSH About dialog

The About dialog (Help -> About) with the TetherSSH mark - a secure anchor tethering out to terminal nodes, rendered in the active theme's accent. Multi-session SSH, telnet, and serial in one tabbed terminal.

Session Manager with Tree View

TetherSSH Session Manager

Hierarchical folder/session tree with search filter, connection status indicators, and multi-tab interface (Light chrome)

Serial Console

TetherSSH serial console

A local serial port (/dev/ttyUSB0) open in the same tabbed terminal as SSH - here a Cisco 2911 console at 9600 8N1, rendered through the same gopyte emulation path. Opened from Quick Connect's Serial transport; the serial backend implements the same interface the SSH client does, so theming, scrollback, selection, and logging all work unchanged.

Telnet

TetherSSH telnet console

A telnet session open in the same tabbed terminal as SSH and serial - here telehack.com:23, a public host whose welcome banner renders cleanly because the telnet option negotiation (IAC) is consumed by the backend before any output reaches the emulator. Opened from Quick Connect's Telnet transport; like serial, telnet is a third TerminalBackend behind the same read loop, renderer, and logging, so nothing downstream of the transport changes.

Scrollback with Draggable Scrollbar

TetherSSH scrollback

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

Text Selection and Context Menu

TetherSSH text selection

Click-drag selection with the right-click Copy/Paste context menu

Full-Screen Applications

TetherSSH running htop

htop with full color support (256-color and 24-bit truecolor), proper resize handling, and alternate screen buffer (Cyber theme)

btop - Wide-Character Stress Test

TetherSSH running btop

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.

Independent App and Terminal Theming

TetherSSH Corporate light chrome with neofetch

Corporate light chrome around a dark terminal pane running neofetch - the chrome and terminal are themed on separate axes, so a light application frame can wrap a dark, full-color terminal

Session Editor

TetherSSH Session Editor

CRUD interface for managing sessions and folders with full authentication configuration

Encrypted Credential Vault

TetherSSH credential vault

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

Quick Connect

TetherSSH Quick Connect

Ad-hoc connections without saving a session, with a Credential dropdown to pull username/auth straight from the vault

Settings

TetherSSH settings

The Settings dialog - terminal display, SSH defaults (terminal type and keepalive), logging, behavior, and per-theme color overrides with live preview on the Colors tab


Current Status: Beta (v0.4.2)

What's New in v0.4.2

  • Auto-reconnect on input. When a session drops, the pane stays put - and if you type into it, instead of silently swallowing the keystrokes TetherSSH offers to reconnect. Yes re-establishes the same transport (SSH, telnet, or serial) with the same parameters, reusing the tab and preserving its scrollback. The prompt is raised exactly once per drop, no matter how many keys you hit, and re-arms on the next clean connect.
  • Anti-idle keystroke. An optional timer sends a configurable, non-disruptive keystroke after a quiet interval, to defeat device-side idle timers - Cisco exec-timeout, console servers, session managers - that an SSH/TCP keepalive does not reset. The keepalive keeps the transport up; the device still logs you out for lack of input, and a keystroke is what resets that clock. Off by default; enable it globally on the SSH settings tab (interval + keystroke) with an Inherit / On / Off override and an optional interval per session in every Add/Edit Session dialog. The keystroke defaults to Backspace (a no-op at an empty prompt); Space+Backspace is offered as a fully non-destructive option (types a space, erases it). It fires only after genuine idle and stops the instant you type.
  • Data-driven theme registry. The built-in themes are no longer hardcoded color vars - each is now a ThemeDef (a chrome palette plus a terminal palette) in a registry, and TetherSSH loads any number of additional themes from ~/.tetherssh/themes/*.yaml at startup. Adding a theme is dropping one YAML file in that directory - no rebuild. A user file whose name matches a built-in replaces it; a new name adds a theme. Both Appearance-tab selectors (App Theme and Terminal Theme) read straight from the registry, so every registered theme is selectable on either axis.
  • Per-theme ANSI palette travels with the theme. Each theme carries its own 16-color ANSI set plus background/foreground, so colored output - htop meters, ls, prompts - resolves in palette under the active terminal theme instead of collapsing onto a single shared dark/light pair. An amber-CRT theme renders htop in its amber ramp; a gruvbox theme renders it in gruvbox greens, from the same upstream SGR codes.
  • Sparse themes still render completely. Empty slots derive from their neighbors (secondary from primary, surface from background, hover from surface-variant, input border from foreground, and so on), so a YAML that sets only a handful of colors yields a full, legible palette rather than gaps. See Theme files.
  • Bundled theme pack ships in the repo. A ready-made set of 30 themes converted from the neterm-js palette set (Dracula, Nord, Gruvbox, Solarized, Tokyo Night, amber/green CRT, Borland blue, and more) is included as themes.zip - unzip it into ~/.tetherssh/themes/ and every one is selectable as chrome, terminal, or both. No build step; they're loaded by the same startup directory scan as any other user theme. See Themes and Theme files.

What's New in v0.4.1

  • Telnet support. TetherSSH now opens telnet sessions alongside SSH and serial, in the same tabbed terminal with the same emulation, theming, scrollback, selection, and logging. Quick Connect's Transport selector is now SSH / Telnet / Serial; choosing Telnet takes a host and port (defaulting to 23) and connects - no username or key, since a telnet device's own login prompt arrives as ordinary session data. Verified against telehack.com:23, a public telnet host that does real option negotiation.
  • Option negotiation handled in the backend. Telnet is raw TCP plus an in-band option protocol (RFC 854): every control sequence is introduced by the IAC byte. The backend runs a small state machine that consumes that negotiation, answers it on the socket, and hands the emulator only the application data - so a DO SUPPRESS-GO-AHEAD never leaks into the screen as garbage. It advertises terminal type and window size, accepts the peer's echo and go-ahead suppression, and answers a terminal-type query with XTERM. Crucially it replies to each option request only when the reply would change state, which is what avoids the classic telnet negotiation loop.
  • CRLF and window size. A lone carriage return (what the terminal emits on Enter) is expanded to CR LF on the wire, as RFC 854 and most network gear and console servers expect; literal 0xFF bytes in input are doubled per the spec. The backend advertises NAWS and pushes the real terminal dimensions to the device on connect and on every resize, so a device that honors window size lays out at the right width instead of a hardcoded 80 columns.
  • One terminal, third transport. Telnet is not a parallel code path: it implements the same TerminalBackend interface the SSH and serial backends do, so the read loop, renderer, paste pacing, and session logging are shared unchanged. An interrupted telnet session (remote close or reset) surfaces a clean "connection lost", while closing the tab is a quiet teardown.

What's New in v0.4.0

  • Serial console support. TetherSSH now opens local serial ports (USB-serial adapters, console servers) alongside SSH, in the same tabbed terminal with the same emulation, theming, scrollback, selection, and logging. Quick Connect gained a Transport selector (SSH or Serial); choosing Serial enumerates the attached ports - with USB vendor/product detail where the OS provides it - and lets you set baud, data bits, parity, and stop bits, defaulting to 9600 8N1 (the usual network-console settings). Verified against a Cisco 2911 console at 9600 8N1.
  • One terminal, swappable transport. Serial is not a parallel code path: it implements the same TerminalBackend interface the SSH client does, so the read loop, renderer, paste pacing (already built for serial-style links), and session logging are shared unchanged. A console cable unplugged mid-session surfaces a clean "connection lost" rather than a silent or hung tab.
  • serialterm diagnostic CLI. A small standalone command (cmd/serialterm) lists serial ports and opens a raw console session, sharing the exact serial backend the GUI uses - handy for confirming an adapter and its settings before connecting, or as a field diagnostic.

What's New in v0.3.3

  • Jump host (ProxyJump) support. A session can now reach its target through a bastion: set a Jump Host (with optional port and username) and the connection is tunneled over an SSH session to the jump host first, exactly like OpenSSH's ProxyJump. The jump host's credentials bind to the encrypted vault the same way the target's do - the session stores only a reference, never a secret. Verified end to end against a Cisco core reached through a Linux bastion.
  • SSH keepalive is now active. TetherSSH sends keepalive@openssh.com requests at a built-in interval over TCP-level keepalive and drops a session after several missed replies, so long-idle router and switch sessions no longer time out mid-task. (The interval became a user-facing setting - global and per-session - in v0.4.1.)
  • Vault credential picker everywhere. Choosing a saved credential by name is now available in every session dialog - Add Session, Edit Session, and the Session Editor - for both the target host and the jump host, not just the Edit Session dialog. Picking one stores the vault reference and prefills the visible auth fields for confirmation; if the vault is locked the picker offers to unlock it first. (Previously the Add Session and Session Editor forms had only a raw text "Creds ID" field that required knowing the credential's ID.)
  • Quick Connect is the primary sidebar action. The footer button now opens Quick Connect directly instead of requiring you to select a session first - that select-then-connect step was redundant once the session tree began connecting on its own.

What's New in v0.3.1

  • The left arrow no longer hides characters. Moving the cursor left through a line - left arrow, Ctrl-A to start of line, mid-line insert/delete - used to erase or scramble the characters it passed over. The cause was the terminal treating the cursor-left byte (BS, 0x08) as a destructive delete instead of a pure cursor move, as VT requires; it's now non-destructive. The Backspace key still deletes a character as expected - that's the shell's doing and was never the problem.
  • 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.

Working

  • SSH connectivity with password and public key authentication
  • Serial console connections (local serial ports / USB-serial adapters) in the same tabbed terminal as SSH, with configurable baud / data bits / parity / stop bits (default 9600 8N1); available from Quick Connect (ad-hoc, not persisted)
  • Telnet connections (telnet-only gear, console/terminal servers, reverse-telnet console ports) in the same tabbed terminal as SSH and serial, with IAC option negotiation, CR -> CR LF on the wire, and NAWS window-size signaling handled in the backend; available from Quick Connect (ad-hoc, not persisted)
  • 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 rendering via incremental per-cell updates in both standard and full-screen (htop/btop/vim) modes - only the cells that change are repainted
  • 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, SGR backgrounds, and reverse video drawn through a seam-free background overlay shared by standard and full-screen (vim/htop/btop) modes, so highlights and full-screen UI backgrounds (meter bars, header rows) render correctly in both
  • 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
  • Jump host (ProxyJump) tunneling - reach a target through a bastion, with the jump host's credentials bound to the vault by reference (never persisted)
  • Host key verification (trust-on-first-use) - an unknown host prompts with its key type and SHA256 fingerprint before connecting; on accept, the key is written to known_hosts and verified silently thereafter. A changed key is rejected as a possible man-in-the-middle, with guidance to remove the stale entry if the change is expected. The jump host uses the same policy.
  • SSH keepalive - app-level keepalive@openssh.com over TCP keepalive, so idle device sessions survive; the interval is configurable globally (Settings -> SSH) and overridable per session (blank inherits the global default, 0 disables)
  • Per-session terminal type - the TERM advertised in the pty request is settable per session (blank inherits a global default of xterm-256color) from Add Session, Edit Session, and the Session Editor
  • Vault credential picker in every session dialog - Add Session, Edit Session, and the Session Editor - for both the target and jump host; pick by name, only the reference is stored
  • 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)

Known Issues

  • SSH agent support not fully implemented (the agent auth path is currently a stub)
  • Host key trust-on-first-use treats a stored key of a different type than the one the server offers (e.g. an ed25519 entry when the host presents rsa) as a mismatch and rejects it - the same behavior as strict OpenSSH StrictHostKeyChecking. Recovery is to remove the stale entry from known_hosts and reconnect.
  • At fractional display scaling (e.g. 1.5x/2x) with a non-default per-tab font size, adjacent selection/SGR cell backgrounds may show faint seams. The seam-free background overlay does not composite under the per-tab font-size theme override, so in that one case backgrounds fall back to being painted as TextGrid cell styles. At the default font size the overlay is used and there are no seams.

Key Features

Terminal Emulation

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 - htop, btop, vim - now run through that same incremental diff rather than a full repaint, which is what keeps them responsive even on Windows, where Fyne issues roughly one GPU draw call per cell. See Rendering Architecture for the full picture.

Paste Handling

  • 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.

Scrollback and Selection

  • 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 by the background overlay shared with SGR backgrounds and reverse video, so it shows in full-screen apps (vim, htop, btop) 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.

Session Management

  • 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
  • Auto-reconnect: type into a dropped session's pane and TetherSSH offers to reconnect the same transport in place, keeping the tab and its scrollback (one prompt per drop)
  • Anti-idle: optional timed keystroke that defeats device idle timeouts (e.g. Cisco exec-timeout); global default plus per-session Inherit/On/Off override (see SSH Tab)
  • Optional compact tree - hide the row icons (Settings -> Appearance) to fit more sessions on screen

Import / Export

  • 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.

Authentication Support

  • 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)

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 (written 0600). 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.

Quick Connect

Open File -> Quick Connect for ad-hoc connections without saving a session. The Transport selector switches between SSH, telnet, and a local serial console:

  • SSH - enter host, port, username; choose Password or SSH Key authentication (default key path ~/.ssh/id_rsa, optional passphrase for encrypted keys); or pick a saved vault credential by name
  • Telnet - enter host and port (defaults to 23, swapping with SSH's 22 as you toggle the transport); no auth fields, since the device's own login prompt arrives as session data
  • Serial - pick an attached port from the list (with a refresh button to re-scan after plugging one in) or type one (/dev/ttyUSB0, COM3), and set baud, data bits, parity, and stop bits, defaulting to 9600 8N1

Telnet and serial connections are ad-hoc and live in Quick Connect only; they are not written to the session tree. (Saved telnet sessions - a stable host:port is worth persisting, unlike a serial port name - are planned but not yet implemented.)

Session Editor

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.

Session Persistence

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: Cisco

credsid 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).


Settings

Open Tools -> Settings to open the Settings dialog. Settings are persisted to ~/.tetherssh/settings.json.

Terminal Tab

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

Appearance Tab

Setting Description Default
App Theme Chrome theme (sidebar, tabs, toolbar, dialogs); lists every registered theme - built-ins plus any in ~/.tetherssh/themes/ Cyber
Terminal Theme Terminal-pane theme (background, default text, ANSI palette); same registry, selectable independently of App Theme 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).

Themes

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. Both selectors list every registered theme, so any chrome can wrap any terminal - for example a Corporate chrome over a Light terminal gives a navy application frame around a white terminal pane.

A theme is pure data: a chrome palette and a terminal palette. Themes come from two places, both feeding one registry:

  • Built-ins are registered in code at startup, in this order:

    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
  • User themes load from ~/.tetherssh/themes/*.yaml (and *.yml) at startup and layer on top. The directory is created on first run; drop a file in, relaunch, and it appears in both selectors. Editing a YAML and relaunching is the whole loop - it's a directory scan each launch, no rebuild.

Registry rules:

  • A user file whose name matches a built-in replaces that built-in (override a built-in by shipping your own cyber.yaml). A new name adds a theme.
  • If a file omits name, the filename is used (doom.yaml registers as doom). If it omits label, the name is used.
  • Order in the selectors is registry order: built-ins first, then user themes in load order.

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 foreground, so it stays legible against that palette's own background rather than inheriting the chrome's foreground.

Theme files

A theme YAML has three top-level scalars and two color blocks. Only the colors you want to set are required - anything omitted is derived (see below), so a minimal theme can be a dozen lines.

name: amber-crt           # registry key; defaults to the filename if omitted
label: Amber CRT          # shown in the selectors; defaults to name if omitted
type: dark                # "dark" or "light" - drives the Fyne variant

chrome:                   # the app UI palette (sidebar, tabs, dialogs, buttons)
  primary:           "#ffb000"   # accent: focus, scrollbar, selection base
  secondary:         "#ff8800"   # links
  background:        "#1a0f00"   # app canvas
  surface:           "#241400"   # menus / overlays / panels
  surface_variant:   "#2e1a00"   # buttons / headers
  foreground:        "#ffb000"   # primary text
  input_background:  "#160c00"   # entry fields
  input_border:      "#5a3d00"   # borders / separators
  selection:         "#ffb00040" # selected tree/list row (8-digit hex = alpha)
  hover:             "#3a2400"   # hover state
  error:             "#ff5555"
  success:           "#88cc00"
  warning:           "#ffcc00"

terminal:                 # the terminal pane palette
  background:    "#1a0f00"
  foreground:    "#ffb000"
  cursor:        "#ffb000"   # part of the model; reserved for future use
  selection:     "#3a2400"   # part of the model; reserved for future use
  black:         "#1a0f00"
  red:           "#cc4400"
  green:         "#aa8800"
  yellow:        "#ffb000"
  blue:          "#996600"
  magenta:       "#cc6600"
  cyan:          "#ddaa00"
  white:         "#ffcc66"
  bright_black:  "#5a3d00"
  bright_red:    "#ff6600"
  bright_green:  "#ddaa00"
  bright_yellow: "#ffd700"
  bright_blue:   "#cc9900"
  bright_magenta: "#ff8800"
  bright_cyan:   "#ffcc44"
  bright_white:  "#fff0c0"

Notes:

  • Hex formats: #RGB, #RRGGBB, or #RRGGBBAA (the trailing pair is alpha - useful for translucent selection/hover).
  • Derivation of empty slots: any chrome slot you leave out is filled from a neighbor (secondaryprimary, surfacebackground, surface_variantsurface, input_backgroundsurface, input_borderforeground at ~33% alpha, selectionprimary, hoversurface_variant); empty terminal slots fall back to sane ANSI defaults. A sparse theme therefore renders as a complete, legible palette rather than leaving holes.
  • cursor and selection (terminal block) are in the data model for completeness; today the renderer consumes background, foreground, and the 16 ANSI colors.

A bundled pack of 30 themes converted from the neterm-js palette set (Dracula, Nord, Gruvbox, Solarized, Tokyo Night, amber/green CRT, Borland blue, and more) ships in the repo as themes.zip. Install it once:

mkdir -p ~/.tetherssh/themes
unzip themes.zip -d ~/.tetherssh/themes/

Because both axes read the registry, each is then selectable as chrome, as terminal, or both. (One naming note from that pack: its corporate is shipped as corporate-classic so it doesn't shadow the built-in corporate.)

Colors Tab

The Colors tab provides quick in-app overrides for the three built-in themes without writing YAML. It has a sub-tab per built-in (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.

For user (YAML) themes, the file is the editor - change the values and relaunch. The two mechanisms are complementary: the Colors tab tweaks the built-ins in place; the themes directory adds and edits everything else.

SSH Tab (partially implemented)

Keepalive Interval, Default Terminal Type, and the Anti-idle settings are live - they set the global defaults applied to new connections (each session can override them). The remaining fields are still placeholders pending wiring into the connection path.

Setting Description Default Status
Default SSH Key Default key path for new sessions ~/.ssh/id_rsa Placeholder
Default Port Default SSH port 22 Placeholder
Default Username Pre-fill username for new sessions (empty) Placeholder
Connection Timeout SSH connection timeout in seconds 30 Placeholder
Keepalive Interval Global keepalive interval in seconds (0 disables); per-session override available 60 Live
Default Terminal Type Global TERM for new sessions; per-session override available xterm-256color Live
Anti-idle Send a keystroke after a quiet interval to defeat device idle timeouts; per-session Inherit/On/Off override available Off Live
Anti-idle Interval Quiet seconds before the keystroke is sent (floor 10s); per-session override available 180 Live
Anti-idle Keystroke What to send: backspace / space+backspace / escape / space (global only) backspace Live

Anti-idle is distinct from Keepalive: keepalive keeps the SSH/TCP transport up, while anti-idle sends real input so the device (e.g. a router's exec-timeout) doesn't log the session out. The keystroke goes out only after a genuine idle interval and never while you're actively typing; backspace is a no-op at an empty prompt, and space+backspace is fully non-destructive even mid-command.

Logging Tab

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.


File Structure

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, shared read loop
│   ├── serial_connect.go        # Serial connect path (ConnectSerial) on SSHTerminalWidget
│   ├── telnet_connect.go        # Telnet connect path (ConnectTelnet) on SSHTerminalWidget
│   ├── reconnect.go             # Reconnect-on-input: transport kinds, prompt de-dup, Reconnect() dispatch
│   ├── anti_idle.go             # Anti-idle keystroke engine: AntiIdleConfig, idle-timer loop, ResolveAntiIdle
│   ├── 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
│   ├── memory_monitor.go        # Status-bar memory / GC / goroutine readout
│   ├── 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/reverse-video background overlay (HiDPI-safe snapped runs); active background renderer for both modes
│   ├── terminal_paste.go        # Paste handling - bracketed paste, line pacing, context menu
│   ├── terminal_selection.go    # Text selection and clipboard
│   ├── tetherssh_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                 # Registry-driven Fyne theme + color resolvers (NewNativeTheme, GetTerminalColorMappings)
│   ├── theme_registry.go        # Data-driven theme registry: ThemeDef/Chrome/Terminal, built-ins, LoadUserThemes from ~/.tetherssh/themes
│   ├── bundled.go               # Generated by `fyne bundle` - embedded logo/icon assets
│   ├── pty_unix.go              # Unix PTY implementation
│   └── pty_windows.go           # Windows PTY implementation
├── cmd/
│   └── serialterm/              # Standalone serial console diagnostic CLI (list / open)
├── internal/
│   ├── serialx/                 # Serial transport (TerminalBackend impl) - shared by app + serialterm
│   ├── telnetx/                 # Telnet transport (TerminalBackend impl) - IAC negotiation, CRLF, NAWS
│   └── 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

Building

Prerequisites

  • Go 1.24 or later (matches the go directive in go.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

Development Build

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    # Windows

Builds 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 ./cli

The 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.

Release Build (Recommended)

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 ./cli

Build Scripts

Convenience 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/logs

Each 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.

serialterm (standalone serial diagnostic)

cmd/serialterm is a small, GUI-free companion binary that drives the same serialx backend the app uses. It's the quickest way to confirm an adapter and its line settings before (or instead of) launching the GUI - list the attached ports, or open a raw console on one. Because it pulls in no Fyne/OpenGL, it builds without the CGO toolchain and cross-compiles trivially, so it's easy to drop onto a jump box or a headless host.

go build -o serialterm ./cmd/serialterm
serialterm - serial console diagnostic for TetherSSH
Usage:
  serialterm list
  serialterm open <port> [-baud N] [-databits N] [-parity none|odd|even|mark|space]
                         [-stopbits 1|1.5|2] [-timeout DURATION]
While connected, press Ctrl-] to quit.
  • list enumerates serial ports, annotating USB adapters with their VID:PID where the OS exposes it.
  • open attaches a raw console. Defaults match the app's serial defaults - 9600 8N1 (-baud 9600 -databits 8 -parity none -stopbits 1). stdin is put into raw mode so keystrokes (including Ctrl-C) go to the device rather than the local shell; Ctrl-] (0x1d) is the escape hatch back out.
  • -timeout controls the read deadline: 0 (the default) blocks until a byte arrives; a duration like -timeout 200ms makes idle reads return promptly - useful for observing non-blocking behavior or scripting around a quiet device.

Binary Sizes (typical)

Build Type Size
Debug (full symbols) ~53 MB
Release (stripped) ~26 MB
Zipped release ~12 MB

Dependencies

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

Configuration

Configuration Location

TetherSSH stores all configuration in ~/.tetherssh/:

File Purpose
~/.tetherssh/sessions/sessions.yaml Session definitions
~/.tetherssh/settings.json Application settings
~/.tetherssh/themes/ User theme files (*.yaml); loaded at startup, layered over built-ins
~/.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.

Supported Auth Types

YAML Value Description
password Prompt for password on connect
publickey Use SSH key from key_path
keyboard-interactive MFA/RADIUS environments

Key Path Expansion

The ~ character is expanded to the user's home directory:

  • ~/.ssh/id_rsa becomes /home/username/.ssh/id_rsa (Linux)
  • ~/.ssh/id_rsa becomes /Users/username/.ssh/id_rsa (macOS)
  • ~/.ssh/id_rsa becomes C:\Users\username\.ssh\id_rsa (Windows)

Roadmap

Phase 1: Core Features (Complete)

  • 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 rendering in both standard and full-screen (alternate-screen) modes
  • 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
  • Correct leftward cursor movement (the BS byte is a non-destructive cursor-left, not a delete, so the left arrow/Ctrl-A no longer erase characters) 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)
  • Jump host (ProxyJump) tunneling, with the jump host's credentials bound to the vault by reference
  • SSH keepalive on connections (app-level keepalive@openssh.com plus TCP keepalive; interval configurable globally and per session, 0 disables)
  • Host key trust-on-first-use - unknown hosts prompt with a fingerprint and are pinned to known_hosts on accept; changed keys are rejected. Applies to the jump host too
  • Vault credential picker in every session dialog (Add, Edit, Session Editor) for both target and jump host
  • Serial console support - local serial ports (USB-serial, console servers) through the same terminal as SSH, implemented as a TerminalBackend so the emulator, paste pacing, and logging are shared; Quick Connect Transport selector with port enumeration and baud/data/parity/stop settings
  • Telnet support - telnet-only and console-server gear through the same terminal as SSH and serial, implemented as a third TerminalBackend with in-backend IAC option negotiation (loop-safe, replying only on state change), CR -> CR LF on the wire, and NAWS window-size signaling; Quick Connect Transport selector adds Telnet (host/port, default 23)
  • serialterm standalone diagnostic CLI (list ports / open a raw console), sharing the GUI's serial backend
  • Help / About menu

Phase 2: Stability (Current)

  • Finish SSH agent support (the agent auth path is currently a stub)
  • Saved telnet sessions - persist telnet as a session protocol (a protocol field defaulting to ssh for backward compatibility), so a telnet host:port can live in the tree like an SSH session; today telnet is Quick Connect only
  • Expose the keepalive missed-reply count as a setting (the interval is now configurable per session and globally; the missed-reply count remains a built-in default)
  • Consolidate the dual render path (see Appendix A) and re-enable repaint coalescing
  • More cross-platform testing (Windows, Linux, macOS)

Possible Phase 3: Logging and Security

  • Log viewer/browser
  • Optional auto-lock after inactivity

POssible Phase 4: Terminal Features

  • Split panes (horizontal/vertical)
  • Find in terminal output
  • Clickable URLs
  • Command snippets/macros

gopyte Terminal Emulation

gopyte is a terminal emulation library built specifically for TetherSSH. It provides:

Screen Hierarchy

  • NativeScreen: Base screen buffer
  • HistoryScreen: Adds scrollback history
  • WideCharScreen: Adds wide character support and alternate screen buffer

Key Features

  • 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

Escape Sequences Supported

  • 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)

Rendering Architecture

Drawing a terminal cell is two separate jobs: the glyph (a character in a foreground color) and the cell background (an SGR background color, a reverse-video swap, or a selection highlight). TetherSSH splits those jobs across two canvas layers and runs the same drawing pipeline for both the standard shell view and full-screen apps.

The layer stack

The terminal pane is a stack of three canvas objects, back to front:

  • Base fill - one rectangle in the terminal theme's background color.
  • Background overlay (bgLayer) - draws every cell background as a set of coalesced rectangles. Adjacent cells of the same color become a single rectangle with edges snapped to whole device pixels, which eliminates the sub-pixel seams Fyne's per-cell TextGrid backgrounds leave on HiDPI displays. Reverse-video cells and selection highlights are drawn here too, through the same path as SGR backgrounds.
  • Glyph grid (TextGrid) - the characters themselves, on a transparent background so the overlay shows through.

Keeping backgrounds off the glyph grid is deliberate: it gives one seam-free renderer for SGR color, reverse video, and selection, instead of three mechanisms competing over the grid's cell styles.

Incremental glyph updates

setStyledRows is the single function that writes to the grid. Instead of rebuilding the grid from a string each frame, it diffs the rows it is handed against what the grid already shows and rewrites only the cells whose rune or color actually changed (via SetCell, whose refresh touches just that cell's canvas objects). A frame in which nothing changed costs nothing; a typing frame updates two or three cells.

This is the difference that made Windows usable for full-screen apps. Fyne issues roughly one GL draw call per cell, so a full repaint of a ~100x32 screen is on the order of 3,000 draw calls every frame - which is what btop's continuous output produced. The diff turns that into draw calls for only the cells that moved between frames.

One subtlety lives here: the grid carries a single extra empty "sentinel" row below the visible content, because Fyne's cell refresh refuses to repaint the last row in the grid. The sentinel sits past the grid's resized height and is clipped, so it is never seen, but it keeps the real bottom line - a shell prompt, or a full-screen app's status bar - refreshable.

One pipeline, two modes

Standard mode and full-screen (alternate-screen) mode run the same three steps every frame, in renderNormalModeUnified and renderAlternateScreenUnified:

  1. setStyledRows(lines, attrs, selection) - diff the glyphs onto the grid.
  2. bgLayer.Update(attrs, selection) - rebuild and paint the backgrounds. This call's canvas refresh is also the frame's paint flush.
  3. updateUnifiedScrollBar(viewport) - sync or hide the scrollbar thumb and flush the scroll container.

The two modes differ only in the viewport, not in how anything is drawn:

  • Standard mode sizes the grid to the visible window and scrolls through scrollback history; the scrollbar is live.
  • Alternate-screen mode (htop, btop, vim, less) sizes the grid to exactly the screen and disables scrollback - full-screen apps own the screen and manage their own layout - so the scrollbar thumb is hidden.

Earlier, full-screen mode used a separate renderer that rebuilt the grid from a joined string and repainted everything each frame. Two things followed from moving it onto the shared pipeline. First, the full repaint became the same cheap diff standard mode uses - the Windows fix. Second, because that old renderer had painted backgrounds itself and fed the overlay nothing, the first cut drew the glyphs but left every background unpainted (htop's meter bars and header, btop's gradient blocks vanished); feeding bgLayer.Update the real per-cell attributes - exactly as standard mode does - is what restored them. Glyphs come from the grid, backgrounds come from the overlay, and both modes now agree on that.

Color resolution

A cell's colors are resolved in one place, cellColors, shared by both modes:

  • 16-color names follow the active terminal theme; the 256-color cube, grayscale ramp, and 24-bit truecolor render their exact values and are memoized (btop re-emits the same few hundred hex colors every frame, so parsing them once per distinct color rather than once per cell is the dominant truecolor saving).
  • Bold promotes the foreground to its bright variant, the standard terminal convention (bold red -> bright red). Bold black is special-cased to a legible color instead of the near-invisible "bright black" grey, because tools like htop use bold-black for primary text.

Font-size override fallback

The per-tab font-size feature wraps the terminal in a Fyne theme override. The background overlay does not composite under that override, so in that one case backgrounds fall back to being painted as TextGrid cell styles rather than overlay rectangles. This is the source of the faint-seam caveat in Known Issues: at fractional display scaling with a non-default font size, the seam-free overlay is bypassed. At the default font size the overlay is always used.

Note: this section describes rendering within the maintained unified path. A separate, older "direct" render path still exists and is documented in Appendix A; consolidating the two remains open work.


Related Projects


License

MIT License - See LICENSE file


Last updated: June 9, 2026

Author: Scott Peterman

About

A Go-based SSH terminal emulator with session management, built on Fyne 2 GUI framework and the gopyte terminal emulation library.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages