Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
d4b3f87
feat(theme): define the v1 role contract
michabbb Jul 27, 2026
161e1b2
feat(theme): parse span-aware theme definitions
michabbb Jul 27, 2026
80b06f7
fix(theme): reject wrong-shaped component sections and gradient misuse
michabbb Jul 27, 2026
969878c
feat(theme): validate the v1 schema
michabbb Jul 27, 2026
2d5ba0d
fix(theme): score role suggestions on the diverging path segments
michabbb Jul 27, 2026
4faeb79
feat(theme): resolve inherited runtime themes
michabbb Jul 27, 2026
519b1c1
fix(theme): report inherited perimeter misuse and unused palette entries
michabbb Jul 27, 2026
199cd1b
feat(theme): embed reference themes and registry
michabbb Jul 27, 2026
db96cde
fix(theme): guard the role matrix, limit boundaries and skipped theme…
michabbb Jul 27, 2026
67a1fe1
feat(theme): render static gradients
michabbb Jul 27, 2026
a7dc697
fix(theme): pin paint_color_at anchoring and share the test allocator
michabbb Jul 27, 2026
537b6d8
feat(theme): own the active runtime theme
michabbb Jul 27, 2026
3b36dc7
fix(theme): check packages as packages and lock down the resolved theme
michabbb Jul 27, 2026
3500f48
feat(theme): add headless theme commands
michabbb Jul 27, 2026
f742dc6
fix(theme): fail a theme check whose directory cannot be listed
michabbb Jul 27, 2026
534ec44
fix(theme): keep the failed themes directory and derive the active id…
michabbb Jul 27, 2026
bb78eb9
refactor(settings): support theme actions
michabbb Jul 27, 2026
170eee7
feat(theme): manage picker preview state
michabbb Jul 27, 2026
c1e8aba
feat(theme): render live theme previews
michabbb Jul 27, 2026
ff79c4e
fix(theme): make the preview reachable and gradient-aware
michabbb Jul 27, 2026
a7bd70e
feat(theme): inject runtime styles into shared chrome
michabbb Jul 27, 2026
f129fc9
refactor(theme): make the activation seam exclusive
michabbb Jul 27, 2026
bf96cc3
fix(theme): keep theme paint off the PTY in composed transition frames
michabbb Jul 27, 2026
bdb1e70
fix(theme): compose the exit transition's PTY region once, from its s…
michabbb Jul 27, 2026
2929d64
test(theme): stand the exit-slide boundary test on the real rounding …
michabbb Jul 27, 2026
fc4a90f
feat(theme): style dashboard panels by role
michabbb Jul 27, 2026
f2c4072
fix(theme): bind the roles the dashboard migration left dead
michabbb Jul 27, 2026
1ad2c28
refactor(theme): drop the panel roles no caller can reach
michabbb Jul 27, 2026
28925fa
fix(theme): make the panel badge invariant unrepresentable
michabbb Jul 27, 2026
62a2931
fix(theme): couple a panel's badge to the family it frames
michabbb Jul 27, 2026
5a140d0
fix(theme): seal PanelRoles so a hybrid bundle cannot be assembled
michabbb Jul 27, 2026
4391953
feat(theme): migrate overlays and identity cards
michabbb Jul 27, 2026
67c31af
fix(theme): restore popup-title parity, drop dead roles, complete the…
michabbb Jul 28, 2026
bfbcc6f
fix(theme): restore the overlays' legacy cells and witness them indep…
michabbb Jul 28, 2026
e6ba9a0
fix(theme): give each field marker back its legacy cell and pin the w…
michabbb Jul 28, 2026
b7d28df
test(theme): inventory the overlays cell by cell, derived from the re…
michabbb Jul 28, 2026
e592829
test(theme): separate uncoloured cells from the ANSI exception, and c…
michabbb Jul 28, 2026
d0998a7
feat(theme): style domain tabs by role
michabbb Jul 28, 2026
c9adcfa
fix(theme): make the unfocused SFTP cursor visible and free the queue…
michabbb Jul 28, 2026
3ab0586
fix(sftp): clamp the queue notice in terminal cells, not scalars
michabbb Jul 28, 2026
16ffb7f
fix(sftp): clamp the whole queue strip in cells, never in scalars
michabbb Jul 28, 2026
66c113a
refactor(theme): retire the fixed renderer palette
michabbb Jul 28, 2026
79de1d1
fix(theme): give the tunnel form title back its inherited accent cell
michabbb Jul 28, 2026
78ca141
test(theme): assert the exact modifier set the legacy cells carried
michabbb Jul 28, 2026
b660b3b
fix(theme): stable picker row identity, fresh previews, description a…
michabbb Jul 28, 2026
d96b3d7
docs(theme): document runtime customization
michabbb Jul 28, 2026
a235f64
docs(theme): correct the guide, the benchmark claims and the parity t…
michabbb Jul 28, 2026
4576849
docs(theme): bound gradient cost by the frame, and name the third par…
michabbb Jul 28, 2026
9eece87
docs(theme): quote the gradient column in the benchmark frame-time range
michabbb Jul 28, 2026
d8d31dc
fix(theme): reconcile development picker chrome
michabbb Jul 30, 2026
1486806
fix theme rebase review findings
michabbb Jul 30, 2026
b56fe1a
test(tui): isolate dashboard golden from ssh agent
michabbb Jul 31, 2026
1286020
fix(theme): address PR review findings
michabbb Jul 31, 2026
f852a63
fix(theme): resolve PR review findings across CLI, chrome and gradients
michabbb Aug 1, 2026
3f4f142
Merge remote-tracking branch 'origin/development' into feature/theme-…
michabbb Aug 7, 2026
bef75a2
fix(theme): address the final PR #86 review findings
michabbb Aug 7, 2026
31b44ae
fix(config): do not follow a symlink swapped in during legacy migration
michabbb Aug 7, 2026
e399ed3
fix: close the gaps an independent audit found in the last two fixes
michabbb Aug 7, 2026
cdde33c
Merge upstream/development into feature/theme-system
michabbb Aug 7, 2026
ea7f3b0
fix(config): drop the needless borrows CI would have rejected
michabbb Aug 7, 2026
1fad201
docs(spec): themed PTY ground and the two transparency switches
michabbb Aug 10, 2026
3e2bd69
fix(theme): back the remote grid, and let the user release any surface
michabbb Aug 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,77 @@ All notable changes to SSHub are documented in this file.

### Added

- **Runtime theme system** (PR #86 by @michabbb) - SSHub's colours are TOML
files now, not constants. A theme sets any of three layers - your own
`[palette]`, a fixed 25-slot `[semantic]` core, and per-role `[components]`
overrides - plus named static `[gradients]`; everything left out is inherited
from `default`, so changing a single semantic slot recolours every component
that uses it. Every one of the 234 component roles the TUI paints is
addressable, from the dashboard and the popups to tunnels, SFTP, the audit
log, the identity cards and the startup animation. Colours are hex, explicit
`rgb`, or references with `brightness` and simulated `opacity` over an
explicit ground; `"terminal"`, `"auto"` and `"native"` are the three
sentinels. Themes live in `~/.config/sshub/themes/*.toml` (the file name is
the ID) and `config.toml` stores only `appearance.active_theme`.
- **Theme picker** (Ctrl+H → Theme…) - moving through the list previews each
theme on the *whole* interface next to a two-box detail preview, `Esc` rolls
back to what was active, `r` re-reads the directory, and only `Enter` writes.
Invalid themes stay listed with their reason instead of vanishing, and a theme
that mentions roles this version does not know is usable with a warning.
- **Five built-in themes**, embedded in the binary and readable with
`sshub theme show`: `default`, `summer`, `aqua`, `fire` and `high-contrast`.
`default` reproduces the previous appearance cell for cell with three
deliberate classes of exception. Cells that carried a direct ANSI colour are
normalised onto their semantic role. A handful of cells that carried *no*
colour at all now carry one - the host and identity form titles, an idle form
value, the form key hints and the detail-panel field marker - because they
were unthemeable and, on a themed popup ground, not guaranteed readable. And
two places are deliberately **improved** rather than reproduced: the selected
row of the unfocused SFTP pane, which previously had no highlight at all so
the cursor vanished in exactly the pane you were about to Tab back into, and
the SFTP queue warning, which used to sit inside the panel heading and is now
drawn as its own line. Each case is recorded individually in
`assets/themes/default.toml` and the design spec.
- **Static gradients** - named multi-stop gradients in five directions
(`horizontal`, `vertical`, `diagonal_down`, `diagonal_up`, `perimeter`) on
frames, separators and backgrounds, painted by buffer post-processing with no
per-cell allocation. App surfaces and gradients never recolour the embedded
remote session.
- **Themed PTY ground** (reported by @Petyok in PR #86 review) - the two
semantic slots `pty_background` and `pty_foreground` back the embedded remote
grid. `default` declares them as references to `background` and `text`, so a
theme that paints its own ground paints it under the grid too; a theme that
claims no ground of its own falls back to the `canvas`/`text` pair. The two
are always written as a pair: filling only the background left the remote's
default foreground to the emulator, which is what made `summer`
near-unreadable (near-white text on cream), and it broke reverse video.
Colours the remote chose itself are never touched.
- **Transparency is a choice now, and `opaque_background` is gone**
(replaces it, reported by @Petyok in PR #86 review) - SSHub is opaque out of
the box under every theme, and two independent `Ctrl+H` toggles hand a surface
back to your terminal: `SSHub transparent`
(`appearance.transparent_sshub_background`) releases SSHub's own ground,
`Session transparent` (`appearance.transparent_session_background`) releases
the remote grid. Both default to off. The old switch asked the opposite
question and could only ever answer half of it: it filled what was *left
over*, so under a theme that paints every surface it had nothing to do and was
silently inert - which direction is open depends on the theme, and asking to
*release* is the one every theme can answer. Both toggles release the ground,
including the panel bodies a theme paints through `semantic.surface`, and keep
the drawing on it: selection bars, status colours, borders and inverted chrome
stay, because a see-through dashboard still has to show which row is selected. `opaque_background = true` in an existing `config.toml` is
ignored; it described the state that is now the default. There is no
transparency slider and there cannot be one - ANSI has no per-cell alpha, so
only your emulator can blend, and only behind cells left at the default
background.
- **`sshub theme` CLI** - `check`, `list` and `show`, all headless (no TUI, no
database). `theme check` validates strictly and reports `file:line:column`
with `did you mean` suggestions; `theme show --resolved` writes a standalone
document that re-reads to the same theme.
- **Theme documentation** - the full guide is
[docs/theme-system.md](docs/theme-system.md), with the generated role
catalogue and two worked example themes; the measured cost of gradient
rendering is in [docs/theme-render-benchmark.md](docs/theme-render-benchmark.md).
- **Isolated profiles** (issue #17) - each profile owns its hosts databases,
settings, fallback credentials, session logs, and tunnel runtime state; each
profile can select its own SSH config source while the default remains
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ rusqlite = { version = "0.32", features = ["bundled"] }
ssh2 = { version = "0.9", features = ["vendored-openssl"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# Levenshtein suggestions for misspelled theme role/field names in diagnostics.
strsim = "0.11"
toml = "0.8"
toml_edit = "0.22"
tempfile = "3"
Expand Down
52 changes: 49 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Add/edit host form, the rebindable keybindings editor (`Ctrl+K`), and the scroll
![Keybindings editor](https://raw.githubusercontent.com/Petyok/SSHub/main/demo/screenshots/keybindings.png)
![Help overlay](https://raw.githubusercontent.com/Petyok/SSHub/main/demo/screenshots/help.png)

The settings overlay (`Ctrl+H`) — toggle an opaque background, OS logos, quit confirmation, and the startup animation:
The settings overlay (`Ctrl+H`) — make SSHub's own surfaces or the remote grid transparent, toggle OS logos, quit confirmation, and the startup animation:

![Settings overlay](https://raw.githubusercontent.com/Petyok/SSHub/main/demo/screenshots/settings.png)

Expand All @@ -61,11 +61,11 @@ The settings overlay (`Ctrl+H`) — toggle an opaque background, OS logos, quit
- **Ad-hoc connect** - in the fuzzy palette (`/`), typing an unknown `[user@]host[:port]` (IPv6 in brackets supported) that matches no saved host offers a "connect without saving" row; Enter opens an embedded ssh session to it. Input is validated and injection-safe (no leading-dash hosts; destination passed after `--`)
- **Local shell tab** - `Ctrl+Shift+T` opens a session tab running your login shell (`$SHELL`, else `/bin/sh`) with the same detach/close semantics as ssh tabs
- **Audit** — log of all connection events with filtering by status (ok/fail) and time range (today/week/month)
- **Settings overlay** (`Ctrl+H`) — toggle an opaque background (for transparent terminals), OS logos, quit confirmation, and the startup animation
- **Settings overlay** (`Ctrl+H`) — let your terminal show through SSHub's own surfaces or through the remote grid (two separate switches, both off by default), toggle OS logos, quit confirmation, and the startup animation
- **Audit** — log of all connection events with filtering by status (ok/fail) and time range (today/week/month); session connect events record the path to the session log when logging is enabled
- **Session logging** — opt-in capture of PTY session output to `~/.local/share/sshub/profiles/<name>/logs/<host-dir>/` (managed hosts use `{name}-{id}`; pure `~/.ssh/config` aliases without a launcher row may share a directory when sanitized names collide). Enable globally in Settings (`Ctrl+H`) or override per host (`inherit` / `on` / `off`). **Logs capture everything echoed to the terminal, including passwords if they appear on screen.**
- **Mosh transport** — per-host `Transport` field in the host form (`ssh` or `mosh`). Embedded sessions use `mosh` when selected; tunnels and SFTP stay ssh-only.
- **Settings overlay** (`Ctrl+H`) — toggle session logging, opaque background (for transparent terminals), OS logos, quit confirmation, and the startup animation
- **Settings overlay** (`Ctrl+H`) — toggle session logging, let your terminal show through SSHub's own surfaces or through the remote grid (two separate switches, both off by default), toggle OS logos, quit confirmation, and the startup animation
- **Hybrid sources** — hosts from `~/.ssh/config` (read-only) and launcher-managed (full CRUD) merge without duplicates
- **Import/Export**: import from `~/.ssh/config`, Termius backups, PuTTY (a Windows regedit `.reg` export or a Unix `~/.putty/sessions` directory), or mRemoteNG (`confCons.xml`); export managed hosts back to ssh config format. Only SSH sessions are imported (RDP/VNC/telnet entries are skipped), and encrypted mRemoteNG passwords are not decrypted (imported hosts carry no stored secret)
- **Hot reload** — edits to `~/.ssh/config` update the host list live via file watcher
Expand Down Expand Up @@ -199,6 +199,11 @@ sshub sftp get prod-web /var/log/app.log ./app.log
sshub sftp put prod-web ./deploy.tar.gz /tmp/deploy.tar.gz
sshub sftp rm prod-web /tmp/deploy.tar.gz --yes

# Themes (see "Theming" below)
sshub theme list
sshub theme show aqua
sshub theme check ~/.config/sshub/themes/mine.toml

# Audit log
sshub audit list --status fail --days 7
sshub audit stats --days 7
Expand Down Expand Up @@ -233,6 +238,7 @@ one yourself with `sshub completions bash|zsh|fish`.
| Resource | Default path |
|------------|---------------------------------------|
| Config | `~/.local/share/sshub/profiles/<name>/config.toml` |
| Themes | `~/.local/share/sshub/profiles/<name>/themes/*.toml` |
| Databases | `~/.local/share/sshub/profiles/<name>/{launcher,metadata}.db` |
| Logs | `~/.local/share/sshub/profiles/<name>/logs/` |
| Tunnels | `~/.local/share/sshub/profiles/<name>/tunnels/` |
Expand Down Expand Up @@ -343,6 +349,46 @@ Defaults below. Rebind any action with **Ctrl+K** (saved to `config.toml`). Pres
| `f` | Cycle filter (all / ok / fail) |
| `r` | Cycle range (all / today / week / month) |

## Theming

SSHub's colours live in TOML theme files you can copy, edit and switch at
runtime. Five themes ship built into the binary — **`default`**, **`summer`**,
**`aqua`**, **`fire`** and **`high-contrast`** — and your own go in the
selected profile's `themes/*.toml` directory (or `~/.config/sshub/themes/` in
compatibility mode), where the file name is the theme's ID.

```bash
mkdir -p ~/.local/share/sshub/profiles/<name>/themes
sshub theme show aqua > ~/.local/share/sshub/profiles/<name>/themes/mine.toml
$EDITOR ~/.local/share/sshub/profiles/<name>/themes/mine.toml
sshub theme check ~/.local/share/sshub/profiles/<name>/themes/mine.toml
```

Select it in the TUI with **Ctrl+H → Theme… → Enter**: moving through the list
previews each theme on the whole interface, `Esc` rolls back, and `Enter` saves
`appearance.active_theme` to `config.toml`. Nothing else is written.

A theme sets any of three layers — your own `[palette]`, the fixed 25-slot
`[semantic]` core, and per-role `[components]` overrides — plus named static
`[gradients]`. Everything you leave out is inherited from `default`, so
changing one semantic slot recolours everything that uses it. True Color
terminals get the colours as written. The embedded remote session keeps every
colour the remote chose itself; a theme only supplies the ground and the default
foreground the remote left unset.

Three headless commands, all without a TUI or a database:

| Command | What it does |
|---------|--------------|
| `sshub theme list` | Every built-in and user theme with its state |
| `sshub theme show <id> [--resolved]` | The theme's source, or a fully resolved standalone export |
| `sshub theme check <file>` | Strict validation with `file:line:column` diagnostics |

**Full guide: [docs/theme-system.md](docs/theme-system.md)** — the file format,
colour values and simulated opacity, inheritance and `"auto"`, gradient
directions and the `perimeter` rule, the complete role catalogue, every picker
key, the CLI exit codes, and two copy-pasteable example themes.

## Configuration

`~/.local/share/sshub/profiles/<name>/config.toml` in profile mode
Expand Down
107 changes: 107 additions & 0 deletions assets/themes/aqua.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# SSHub — "Aqua": deep water.
#
# What this theme demonstrates:
# * a dark theme with a *painted* background instead of `default`'s
# transparent one
# * a `perimeter` gradient — the one direction that runs a closed ring around
# a frame, so it may only be used on roles that draw a closed frame
# * a horizontal gradient reused by several roles
# * `over` as an explicit mixing ground for `opacity`

schema_version = 1
extends = "default"
name = "Aqua"
description = "Deep-water blues with cyan and turquoise accents, and a ring-lit focus frame."

[palette]
abyss = "#04141c"
deep = "#08202a"
shelf = "#0d3040"
shoal = "#12455a"
turquoise = "#2fd0c5"
cyan = "#6fd8e8"
foam = "#d3f4f7"
kelp = "#4fbf8f"
coral = "#e8746a"
amber = "#e0a95c"
slate = "#5b7f8c"
slate_dim = "#2b4954"

[semantic]
background = "palette.abyss"
canvas = "palette.abyss"
surface = "palette.deep"
surface_raised = "palette.shelf"

border = "palette.slate_dim"
border_focus = "palette.turquoise"
border_popup = "palette.slate"

text = "#c6e6ec"
text_bright = "palette.foam"
text_highlight = "#ffffff"
text_muted = "palette.slate"
text_dim = "palette.slate_dim"
text_inverse = "palette.abyss"

accent = "palette.cyan"
selection_bg = "palette.shoal"
selection_fg = "palette.foam"

success = "palette.kelp"
warning = "palette.amber"
error = "palette.coral"
info = "palette.cyan"
connecting = "palette.turquoise"
exited = "palette.coral"
unknown = "palette.slate_dim"

# A horizontal sweep across the width of whatever it paints.
[gradients.tide]
direction = "horizontal"
stops = [
{ at = 0.0, color = "palette.shoal" },
{ at = 0.5, color = "palette.turquoise" },
{ at = 1.0, color = "palette.cyan" },
]

# `perimeter` walks clockwise around the outer ring of a frame starting at its
# top-left corner. Because the ring closes, the first and the last stop must
# resolve to the *same* colour — otherwise the frame would show a seam. Using
# this direction on a role that does not draw a closed frame is an error.
[gradients.reef_ring]
direction = "perimeter"
stops = [
{ at = 0.0, color = "palette.turquoise" },
{ at = 0.35, color = "palette.cyan" },
{ at = 0.65, color = "palette.foam" },
{ at = 1.0, color = "palette.turquoise" },
]

# Focused frames get the ring; unfocused frames stay solid so focus still reads
# at a glance.
[components.dashboard.host_list]
border_focused = { gradient = "gradients.reef_ring" }

[components.dashboard.details]
border_focused = { gradient = "gradients.reef_ring" }

[components.sftp.panel]
border_focused = { gradient = "gradients.reef_ring" }

[components.popup]
border = { gradient = "gradients.reef_ring" }

# Open (non-framing) roles take the horizontal sweep.
[components.header]
background = { gradient = "gradients.tide" }
separator = { gradient = "gradients.tide" }

[components.separator]
primary = { gradient = "gradients.tide" }

# An explicit `over` names the opaque colour the translucent value is mixed
# into. Without it the ground would be `semantic.background`, which works here
# but would fail on a theme that left the background at "terminal".
[components.footer]
background = { color = "palette.turquoise", opacity = 0.18, over = "palette.deep" }
Loading