Repository navigation
Releases: iksteen/shellglass
Release list
v0.26.0: kitty transfer-then-place, so zellij graphics mirror
Upgrade note: the browser renderer changed, and the hub serves it — update
the hub to get the new painting. Nothing else has to move together: the
push wire protocol stays at 2, and session keys, ids and view URLs are
unaffected. A new pusher against an old hub still mirrors its images, just
without stacking order or sub-cell alignment (the old viewer ignores both);
an old pusher against a new hub is unchanged.
shellglass mirrors kitty's transfer-then-place graphics. Until now only a
direct placement at the cursor (a=T) was mirrored, so a multiplexer that
transfers an image once and then places rectangles of it — a=t followed by
a=p, which is how zellij draws graphics — showed nothing in the browser
while the terminal showed the picture. Placements now carry everything the
emitter states about them: the source-pixel rectangle it cut out, the
sub-cell X/Y offset it positioned that rectangle with, and the z it
stacked it at (below the text, or above it, kitty's rule). a=d deletes are
honoured by image id, by cursor cell, and by geometry, so an image the
emitter revokes without repainting the cells under it disappears from the
mirror at the same moment it does locally. Overlapping and floating zellij
panes composite the way they do on the local screen.
kitty placements survive text drawn over them. The mirror used the sixel
model — printing into a cell erased whatever image covered it — so any pane
repaint punched holes in an image, or lost it entirely, while the terminal
still showed it whole. A kitty placement now lives until its emitter deletes
it, the whole display is erased (CSI 2 J), or it scrolls off the screen,
which is exactly when kitty drops one. Sixel and iTerm2 images keep the old
lifetime, because those really are erased by the text.
Images fill the cells they occupy. Every mirrored image used to be
letterboxed inside its cell box, so the tiles of an image split across panes
stopped abutting and the gaps showed as bars through the picture. An image is
now drawn at the exact cell rectangle it claims, snapped to whole device
pixels so neighbouring tiles leave no seam. The trade: when the browser's
cell shape differs from the terminal's, an image is stretched to fit rather
than letterboxed — configure the mirror with the font you run locally and the
two agree.
Several images placed at once all appear. The decode worker kept only the
newest pending job, so a burst that placed distinct images — a sixel or raw
kitty sequence per pane — left all but the last one invisible until something
redrew. Each placement now keeps its own job.
The vendored vt100 recognizes DECRQM (CSI ? $ p) and OSC 4 palette queries
and ignores them, instead of reporting them as escape sequences the mirror
did not handle.
v0.25.0: detachable sessions mirror inline images
Upgrade note: the attach socket protocol goes from 1 to 2 — update
shellglass push --detachable and shellglass attach together. They are
shipped as separate binaries, so a skewed attach is now refused with an
explicit reason instead of failing halfway through the handshake. Nothing
else moves: the push wire protocol stays at 2 (no coordinated hub/pusher
update), and session keys, ids and view URLs are unaffected.
Detachable push sessions mirror inline images. Until now push --detachable
dropped kitty/iTerm2/sixel graphics, because a terminal-less session has no
terminal to probe for image capabilities. An attach now hands the session its
terminal profile — graphics capabilities, default foreground/background, and
cell geometry — which the session caches: images keep being intercepted,
decoded and mirrored after you detach, using the last attached terminal's
profile, and that terminal's dimensions are retained as before. A later
attach replaces the cached profile only once its own capability probe
succeeds, so a failed or skewed attach never degrades a running session;
before the first successful attach, graphics interception stays disabled.
While detached, a kitty capability query is answered from the cached profile
(file/shm queries are still refused, as on the local mirror). --sixel-compat
now applies to detachable sessions too.
Windows keeps its native mouse gestures. shellglass no longer clears the
console's Quick Edit bit for the whole run, so under Windows Terminal wheel
scrollback and right-click paste keep working while you use an ordinary
shell. Quick Edit is released only while the mirrored program has explicitly
enabled an xterm mouse-reporting protocol, and restored when it stops.
The vendored vt100 recognizes two more mouse encodings: urxvt (1015) and
SGR with pixel coordinates (1016), alongside the existing 1005/1006. A TUI
selecting one is now correctly seen as having mouse reporting enabled —
which is what the Windows Quick Edit handoff keys off.
v0.24.0: wire protocol 2 — update every part together
Upgrade note: this release bumps the push wire protocol from 1 to 2, and
hubs of this build serve protocol 2 only. Update the hub AND every push
client together: a 0.24 hub rejects an older client with 426 Upgrade Required (headers name the hub's version and the protocol range it serves),
and a 0.24 client gets the same 426 from an older hub. Session keys and ids
are unaffected — the id salt is unchanged, so no re-derivation, no --allow
edits, no new view URLs.
Fonts stop crossing the trust boundary as CSS. A client used to push
@font-face rules and family names that the hub re-served; it now pushes
structured, hash-keyed font assets, and the hub derives every content
address, validates that a family key belongs to an uploaded regular face,
picks the format() token, and emits the CSS itself. No client-authored font
syntax or free-form family name is reproduced anywhere. Inline-image blobs
are likewise byte-sniffed to PNG/JPEG/GIF/WebP and served with the canonical
type, so claimed HTML/SVG never becomes a same-origin asset.
Hubs can force a trusted presentation. Every hub embed surface (?embed,
config, style.css) is now hub-owned — built-in template, hub-generated
base CSS and render config — regardless of what the pusher sent; validated
hash-keyed fonts still render, under hash-only family names. For normal
pages, hub --template <path> supplies a fallback and --force-template
makes the hub's presentation authoritative, discarding pushed template, CSS,
and render JSON. Hub-owned templates get a per-response {{nonce}} and a
matching script CSP. Standalone serve keeps using its local configured
presentation.
One pusher per session, enforced. A new /push connection atomically evicts
the incumbent, which is closed with a private replacement code and exits
instead of reconnecting — so two clients holding the same key can no longer
steal the session back and forth in a loop. (This contract is the second half
of the protocol bump: a protocol-1 client treats every close as transient.)
Detachable push sessions, the dtach(1) model. push --detachable runs the
mirrored command in a terminal-less PTY that keeps streaming to the hub with
no local terminal, exposed on a unix socket; shellglass attach <socket>
connects a real terminal (driving input and size) until Ctrl-\ detaches it,
and the session then holds the last attached size. One client at a time —
a second attach is refused unless it takes over with --force. Adding
--daemon forks the push into the background so it survives an SSH logout.
Unix only; inline images aren't mirrored in this mode (no local terminal to
probe for image capabilities).
shellglass is a reusable library. An external frame producer can drive the
whole stack — stock viewer, diff protocol, local HTTP/SSH serving, recording,
hub push — through api.rs (Presentation, serve/serve_with_shutdown,
push) and the source.rs producer boundary, with no PTY and no vt100. The
CLI runs through the same API, so the two cannot drift. See
docs/library-api.md and examples/external-source.rs.
Windows support, contributed by Duncan Ogilvie (@mrexodia). shellglass runs
on ConPTY (Windows 10 1809 or newer; Windows Terminal recommended), defaults
to %COMSPEC%, and tracks console size by polling. The release archives now
include Windows x86_64 binaries, and CI builds Windows on every push.
Also:
- Unix release archives bundle eight binaries, including the new
shellglass-attach; the Windows zip keeps seven (attach is Unix-only). - Canvas glyphs are crisp in Safari. Both built-in pages fit the terminal
withtransform: scale, and WebKit was rasterizing the transformed canvas
layer at its own density and resampling it. The canvas now mounts in a
non-transformed#sg-canvas-hostbox that mirrors the scaled rectangle
(snapped to whole device pixels); only the transparent ghost rows stay
under the transform. Custom templates can opt in with the same id. - Canvas text is pinned to grayscale antialiasing, so Safari's stem darkening
no longer double-applies on top of the weight boost. - The CRT effect is gone: the tube styling, scanline/bloom/vignette overlays,
the#crtnav toggle,?crt, and its localStorage preference. The page
keeps the flat look the CRT-off state rendered. It may return in a better
form later.
v0.23.1: fix warped phone rendering of wide terminals
Wide terminals mirrored to a phone rendered warped: mobile WebKit
enforces a minimum effective font size, and the CSS zoom the viewer
templates used to fit the terminal counts toward it — a wide session
fit at zoom ~0.2 got its ghost text boosted ~4x (ignoring
text-size-adjust, so v0.23.0's opt-out didn't cover it), inflating the
terminal box and unseating the canvas glyphs. Both templates now fit
with transform: scale, which the autosizer never sees; fit, pinch/
ctrl+wheel zoom, and pan behave as before, verified across WebKit,
Firefox, and Chromium.
Updated push clients deliver the fixed page through existing hubs
verbatim; the embed view (iframe or ) is served by the
hub itself and picks the fix up when the hub is upgraded.
v0.23.0: session recording, recording management, iPhone rendering fix
Session recording. --record-dir <dir> on serve and hub records sessions
as timestamped native shellglass streams (.sgs, JSONL): the push
transcript itself — register, image payloads, wire messages, verbatim — so
a recording is self-contained (fonts, template, and images ride along) and
replayable at full mirror fidelity by anything that speaks the wire format.
The hub writes one file per push connection under <dir>/<session-id>/; a
pusher declines with push --no-record (or SHELLGLASS_NO_RECORD=true). No
wire-protocol changes: existing clients and hubs interoperate unchanged.
Recording management, two credentialed surfaces with the same verbs — list,
retrieve, delete — differing only in scope. The management API reaches any
session's recordings (GET/DELETE /api/sessions/by-{id,slug}/…/recordings[/name],
driven by the new shellglass sessions recordings list|get|delete --id|--slug); session owners reach only their own, authorized by the
session key itself (/recordings[/name], via the new shellglass recordings <url> list|get|delete subcommand or the slim shellglass-recordings
binary). Recordings are plain files: rotation and retention are the
operator's, and files outlive session deletion (still listable by id).
iPhone rendering fixed. iOS Safari's text autosizer inflated the terminal's
text (landscape phones especially), distorting the whole terminal box to
roughly double width and drawing glyphs far too big for the grid. The
screen now opts out (text-size-adjust: 100% — user pinch-zoom is
unaffected) in the pushed CSS, the placeholder page, and the
viewer-injected rules; updated push clients deliver the fix through
existing hubs verbatim, and iframe-less embeds pick it up from an updated
hub's viewer.js.
Also:
- gen-key and print-id merged into one
shellglass-keytoolbinary.
BREAKING (standalone binaries only):shellglass-gen-key/
shellglass-print-idare gone — useshellglass-keytool gen-key/
print-id, or the unchangedshellglass gen-key/print-id
subcommands. Keys and ids are byte-identical. - Release archives now bundle seven binaries, including the new
shellglass-recordings.
v0.22.1: per-font weight boost, grow-to-fit template, config env var
The built-in template now grows the terminal to fill its stage, not just shrink
it. A mirror smaller than the viewport used to sit at its natural size in the
top-left; it now scales up to fit as well as down, so the terminal fills the
window at any size.
The renderer's weight boost is now a per-font setting. The kitty-parity
double-draw thickens anti-aliased midtones, which some fonts' own hinting
already handles — on those it reads as too heavy. Set weight_boost = false in
a [fonts."Name"] entry to turn it off for that family; omit it to keep the
boost, so every existing config renders exactly as before.
The config path now falls back to $SHELLGLASS_CONFIG when --config is
absent, so a shell profile can point at a config once instead of on every
invocation. An explicit --config still wins.
v0.22.0: hub Docker image, all binaries shipped, live config reloads
The Docker image is now a hub server. It ships the per-mode shellglass-hub as
its entrypoint, so hub flags pass straight through:
docker run <image> --bind 0.0.0.0:8080 --allow <id>:<slug>
BREAKING (image invocation only): the old docker run <image> hub … form —
hub as a subcommand of the universal binary — no longer works; drop the hub
word. Nothing else changes: session ids, secret keys, and the push/viewer
protocols are all untouched, so no re-derivation and no client/hub upgrade
coordination is needed.
Release archives now bundle every binary. The build always produced all seven —
the universal shellglass plus per-mode serve/push/hub/sessions/gen-key/
print-id — but the tarball shipped only the universal one. Each
shellglass-.tar.gz now contains all seven, so a hub-only box can run the
smaller shellglass-hub without building from source.
Open mirrors reload themselves when the fonts or CSS change. A viewer bakes its
@font-face rules into the page at load time and frames carry only cell data, so
restarting a push client (or a serve process) with a different --config used to
leave every already-open tab on the old fonts until a manual refresh. The
hub/serve now tag their rendering config and announce it to viewers, which
re-fetch when the tag they loaded with stops matching. Iframe-less embeds can't
reload the host page, so they fire a shellglass-reload event for the host to
handle instead. No protocol bump — it rides an SSE event older viewers ignore.
The bundled config.example.toml no longer detaches powerline arrows: it mapped
the E0B0-E0B3 separators to a font, overriding the viewer's built-in geometry
that bleeds each arrow flush against its segment bar. It now maps only the
codepoints that need a font (E0A0-E0AF, E0B4-E0D4) and leaves the arrows to the
seamless geometry.
No more spurious "not mirrored — please report" notices for sixel display and
scroll modes: the vendored vt100 now treats DECSDM (CSI ?80h/l) and the xterm
sixel-scrolling modes (CSI ?8452h/l) as deliberately ignored — shellglass
handles sixel through its own image path — instead of flagging them unhandled.
v0.21.0: version negotiation, image fidelity, SSH view niceties
Version negotiation — and the last session-id rotation. Until now, any breaking
change to the push<->hub wire format bumped the session-id salt, which changed
every session id: every such upgrade meant re-running print-id and updating
--allow and view URLs. That is now over. The push client sends the wire protocol
it speaks, and a hub that can't serve it replies with a clear error naming its
exact version and which side to upgrade — instead of a silent stall or a
misleading "unknown key". From here the salt is frozen: future wire changes
negotiate a protocol number, so session ids never move for a protocol change
again.
ACTION REQUIRED, once: v0.21.0 carries the final salt bump (v5 -> v6), so it
rotates session ids this one last time. Re-run print-id --key <secret> and
update the hub's --allow (and any --sessions-file and shared view URLs). Your
secret keys stay valid. A v0.21.0 client against an older hub, or vice versa,
now fails loudly with a version message rather than silently.
Inline images no longer bleed into the line below. A sixel/kitty/iTerm2 image
whose pixel height wasn't an exact multiple of the cell height claimed one extra
full row on the web mirror, so the shell prompt printed on that row drew on top
of the image. Images now occupy their true (fractional) height, matching what
the terminal shows. Producer-side fix — effective once the serve/push side is on
v0.21.0, no viewer change.
SSH view: centering and a MOTD. The read-only SSH view now centers the session
when the viewer's terminal is larger than the operator's, horizontally and/or
vertically, instead of pinning it to the top-left. New --ssh-motd-file <path>
shows a banner when a client connects (any control characters allowed, so ANSI
art works), held for --ssh-motd-delay <n> seconds (default 5).
A one-shot /snapshot endpoint. Each session now serves GET .../snapshot: a
single JSON blob of the current screen — the same full-frame message a new
viewer receives first — without opening an SSE stream. On both serve and hub.
/api/sessions reports activity. The management API's session list now shows
whether each session is live and its viewer counts per transport (web, ssh), so
shellglass sessions list tells you what's actually being watched.
Hub operators: deploy the new hub for negotiation, /snapshot, the activity
listing, and the SSH features; the image fix rides the serve/push binary. Because
of the salt bump, rotate ids as noted above when you upgrade.
v0.20.1: static Linux binaries, steady mirror cursor
Fully static Linux binaries. The released Linux binaries (and the Docker image)
are now statically linked — no libc dependency, so one binary runs anywhere:
Alpine, distroless, or a host with an older glibc. Especially handy for the push
client — scp it onto any box and run it. macOS binaries are unchanged.
Steady cursor in the web mirror. Apps that bracket each redraw in hide/show
cursor sequences — a "thinking" spinner does this ~14×/s — made the mirrored
cursor flicker on and off, because the 30fps sampling caught the brief hidden
gap that the local terminal never shows. The mirror now holds the cursor
steady through a redraw, matching the terminal. Fixed in the producer, so it
takes effect once the serve/push side is on v0.20.1 (no viewer change).
Window title as an event in embedded mode. The iframe-less (light/shadow)
embeds never touch the host page's tab title; the session's window title now
rides a shellglass-title CustomEvent on the element
(event.detail = the title, "" on clear), so the host page decides what to do
with it:
el.addEventListener("shellglass-title", e => { document.title = e.detail })
No protocol version bump — clients, hubs and viewers interoperate unchanged.
Hub operators: the title event ships in the viewer.js / embed.js the hub
serves, so deploying this delivers it to embedded sessions; the steady-cursor
fix lives in the pusher binary instead, so a pusher gets it by upgrading to
v0.20.1.
v0.20.0: iframe-less embedding
Embed a live terminal in another page without an iframe.
now renders three ways, chosen with a mode attribute (or
data-mode on the one-liner script):
- light (default) — renders in the host page's own DOM. No frame to fight a
strict CSP or to style around; native selection and copy. - shadow — renders in a shadow root, style-isolated from the host page.
- iframe — the classic sandboxed frame (the previous behavior).
The embedding contract is unchanged: the one-liner
<script src="https://hub/embed.js" data-src="https://hub/s/demo"></script>
and the element still work; only the default
rendering flips to iframe-less.
Sizing. An unstyled light/shadow embed is the terminal's natural size. Give the
element a CSS width and/or height and the terminal scales to fill that box,
letterboxed (aspect preserved) and re-rasterized crisp as it scales — so it
behaves like a normal sized element.
Cross-origin. Same-origin is the easy path: put the hub behind a reverse proxy
on your own domain and point the embed at that path — every asset the viewer
fetches is relative, so nothing is cross-origin and no CORS is needed. For a
genuinely cross-origin iframe-less embed, run serve or hub with
--cors-origin <origin> (repeat for several, or * for any). The iframe mode
needs none.
No protocol version bump — existing clients, hubs and viewers interoperate
unchanged (the one added register field is optional, with a fallback). Hub
operators: the embed changes ship in the viewer.js / embed.js the hub serves
and in two new per-session routes, so deploying this to the hub is what
delivers iframe-less embedding of its sessions.