Skip to content

0.2.0 — 2026-06-16

Latest

Choose a tag to compare

@github-actions github-actions released this 17 Jun 00:59

Release Notes

This release is the deferred-polish + audit sweep — a session built on
four explicit phases: ship the open backlog, drive multi-persona testing
through the headless harness, fix every verified finding, and audit the
public site against ghostty.org. Across 36 commits, shell.rs split
from 4135 → 561 lines (-86%)
via 10 sibling-module extractions; 21
persona-testing findings shipped (4 modifier-guard families, 4
per-window scope inconsistencies, 5 mouse-access via context menu, 4
modal-stacking, 3 headless quirks, plus the help-overlay.gif regression
re-record); 5 ghostty-comparison doc edits landed (landing reorder,
themes quickstart, install per-OS routing, About page, shell-integration
bash/fish). Final state: 544 lib tests pass, Starlight build clean at
28 pages, every audit finding adversarially verified.

Highlights:

  • Shell.rs split — extracted into 8 method submodules (resize,
    scroll, io, queries, render, spawn, accessors, reader) + 9 sibling
    helper crates (shell_cell_attrs, shell_color, shell_config,
    shell_context, shell_keys, shell_mouse, shell_osc1337, shell_prompt,
    shell_pty). Submodule visibility lets each slice touch private
    ShellSession fields without pub(crate) leakage.
  • Persona-testing fixes — keyboard chords no longer get eaten by
    overlay filter buffers (Cmd+Shift+R no longer wipes recents), per-
    window cfg overrides honored by toggle_sidebar / toggle_tab_bar,
    pane management reachable from the right-click menu (Split / Close /
    Zoom / Equalize), pane divider gets a hover cursor, Cmd+W closes
    the topmost overlay from inside palette/find/etc., view.help no
    longer stacks on other overlays, headless --app no longer
    persists user config when toggle buttons are clicked.
  • Render perf — apply_to_grid drops the parser Mutex before the
    rustybuzz ligature pass; sync-depth (DECSET 2026) early return
    reads a cached cursor instead of leaking the live one; per-frame
    ~4KB palette_override.indexed clone replaced with a lock-held
    borrow; per-frame link_grid / underline_grid /
    underline_color_grid clones now gated on sticky-true *_active
    flags so sessions that never see OSC 8 / extended underlines /
    CSI 58 skip the clones entirely.
  • Settings save-failure inline error banner — Save now happens
    before the overlay closes; failure stays the dialog open with a
    red ! save failed: <reason> banner instead of silently losing
    the user's edits.
  • Type-to-filter the help overlay — start typing while ⌘⇧/ is
    open to narrow the binding list.
  • 20 demos shipped across the manual — 3 layout demos
    (many-tabs-horizontal / bottom-tab-bar / tab-layout-toggle), help-
    filter, attention-dot, palette-filter, settings-window-scope,
    discovery-install, help-themes, plus the existing 13.
  • Site audit fixes vs ghostty.org — landing reordered so the
    plain-English value pitch precedes the jargon grid, themes
    quickstart added, install page gains per-OS jump table + version
    banner + Gatekeeper escape hatch, new /about page, shell-
    integration grew bash + fish recipes + verification step.

Menu bar

  • New Edit > Find… menu item (⌘F). Standard macOS slot, mirrors the
    existing chord. Opens the in-pane find bar in the focused pane.
  • New Edit > Find Next and Edit > Find Previous menu items.
    Work whether the find bar is visible or not — as long as a find
    session is active, they advance through matches. Same match-
    advance logic Enter / Shift+Enter use inside the find bar.
    Intentionally no chord — ⌘G / ⌘⇧G stay reserved for the
    fwd.cmd_g forwarder that sends ⌘G as ⌃G to native mnml panes
    (goto-line in the editor).
  • New Shell > New Tab (⌘T) and Shell > Close Tab (⌘W) menu
    items. Standard slots that were previously chord-only.
  • New View > Recents… (⌘R) menu item. Opens the recents picker —
    the same overlay the welcome screen used to surface on startup.
    Mirrors the existing chord; gives the recents flow a discoverable
    menu-bar entry point.
  • New View > Search Tabs… menu item. Toggles the
    tab-search overlay (filter tab chips by name). Intentionally
    no chord — ⌘⇧T is reserved at the chord-registry layer for
    tab.reopen_closed (browser convention).
  • Help > tmnl Help now opens the in-app help overlay (same surface
    ⌘⇧/ opens) instead of logging a placeholder. The overlay lists every
    registered command + its current chord — the discoverable answer to
    "what does tmnl do?" for new users.

Multi-window

  • ⌘W on the last tab of one of N windows now closes only that
    window
    , instead of quitting tmnl and dropping every other open
    window with it. When the focused window's last tab closes and at
    least one background window is open, swap a background window in
    as the new focused window (dropping the previous one's winit
    handle closes the OS window). When no background windows are
    open, the historical "quit the app" behavior still fires.
  • ⌘W no longer cascades through every open window in one
    press. Dropping a WindowState causes AppKit to queue a
    CloseRequested for the just-closed window. The handler used to
    process that stale event against the new focused window and
    cascade-close. Now the dispatcher ignores WindowEvents whose
    WindowId doesn't match any window we still own.
  • Persistence stays in sync with what's actually open
    closing one of N windows (via the red close button or via the
    last-tab ⌘W path) now writes the smaller window-state immediately,
    so the next launch doesn't resurrect the closed window.

Chrome strip

  • Back / forward arrows on Shell panes now jump to the previous /
    next OSC 133 prompt in scrollback (same as ⌘↑ / ⌘↓). They used to
    forward Ctrl+PgUp / Ctrl+PgDn to the focused pane — useful on
    Native (mnml) panes, no-op in bare zsh. The chord stays in place;
    the arrows give it a discoverable click target.

Selection

  • Shift+click now extends the existing body-selection — anchor stays
    put, focus moves to the click position — instead of starting a fresh
    single-cell selection. Falls back to a fresh selection when there's
    no existing one on the same tab + pane. Matches the macOS Terminal /
    iTerm / VS Code convention.
  • The selection highlight now paints during drag, not only on
    release. Each CursorMoved while dragging force-pushes the live
    bounds to gpu.selection_bounds so the next frame paints them even
    if the redraw chain lagged.

Modal overlay accessibility

Every modal overlay — palette, palette overlay (chip-clicked),
welcome, discovery, settings, help, right-click context menu, and
both confirm dialogs (close + paste) — now has full mouse +
keyboard parity:

  • Click a row to activate / focus it (palette dispatches the
    command, welcome opens the entry, discovery toggles in-rail,
    settings focuses the row; second click on the focused settings
    row cycles its value).
  • Wheel scrolls the row selection on welcome / discovery /
    settings / help.
  • Click outside the panel dismisses (palette / palette overlay /
    welcome / discovery / help / context menu); settings is
    intentionally excluded to avoid silently discarding unsaved
    row edits.
  • Confirm-close and confirm-paste dialogs grow visible [Close] / [Cancel] and [Paste] / [Cancel] buttons. Click-outside
    also cancels.
  • Hover pre-selects rows on welcome + discovery — standard menu
    UX, lets users hover-then-Enter without a click.
  • Help overlay click-outside dismiss covers chrome / chip clicks
    that previously slipped past the body-click-dismiss path.

Keyboard chords (closing the persona-report gaps)

  • F2 — rename the focused tab. VS Code parity. Same modal as
    the right-click rename path; first-class menu item lives under
    Window > Rename Tab.
  • ⌘⇧1 … ⌘⇧9 — open launcher rail icons. Closes the "rail is
    mouse-only" gap. Out-of-range indices silently no-op.
  • ⌘Home / ⌘End — jump to top / bottom of scrollback. Pairs
    with the existing ⇧PgUp / ⇧PgDn row-by-row paging.
  • ↑ / ↓ in ⌘F find bar and ⌘⇧T tab-search step matches
    forward / backward. Was Enter / Shift+Enter only.
  • ⌘⇧P palette and ⌘R recents now supersede transient input
    overlays
    — welcome / tab_search / find / palette_overlay /
    discovery / context_menu auto-dismiss as the new one opens.
    The "destructive" modals (settings / help / rename / confirm
    dialogs) still block. Closes the 06-10 hybrid SEV-4 #7 design
    call.
  • ⌘D in a Browser pane forwards as ⌃D instead of unconditionally
    splitting right, so mnml's multi-cursor "select next
    occurrence" chord works when the editor's a Native pane.
  • ⌘Z / X / S / N / P / B / G / / no longer silently eat keys in
    Shell context.
    The fwd.cmd_* chords now gate on Native
    focus; in Shell context they fall through to the pty's char
    path as the user expects.

Browser pane chord coverage

Safari conventions, all gated on Browser-pane focus:

  • ⌘[ / ⌘] — history back / forward.
  • ⌘L — focus the URL bar (seeds the chrome edit buffer with
    the current URL).
  • ⌘R — reload (rides on view.recents's wire; falls through
    to opening recents when no Browser pane is focused).

Maximize Pane (zoom)

The Shell > Splits > Maximize Pane (⌘⇧↩) verb shipped in
0.1.5 but was substantially broken — composite painted via
tab.layout.leaf_rects instead of effective_leaf_rects, so
the zoomed pane's full-area grid only painted at its small
underlying split-tree rect; pane_under_cursor mis-routed
clicks; divider drag still fired through the zoomed pane;
close / split paths left zoomed_pane stale across id shifts.
All fixed; the menu label now flips between Maximize Pane
Restore Pane in sync with the state (across toggle / chord /
menu click / tab switch / window swap / close / split paths).
Active chip in the strip also gains a leading badge while
zoomed.

Multi-window state drift

Several latent state-drift bugs that surfaced as part of the
zoom-feature audit:

  • Per-tab modals (confirm_close, renaming_tab, find,
    text_selection) carry tab_idx references that were stale
    after a tab close, tab reorder, or window swap. Now shifted /
    cleared in lockstep at every site (close_tab_at_unchecked,
    swap_tabs, focus_background_window,
    spawn_in_process_window, red-X close, drain restore).
  • dragging_scrollbar (PaneId), last_hover_native (PaneId),
    fim_pending / ghost (AI completion source) — same pattern,
    cleared at the right shape on tab close / tab switch.
  • view.settings toggle (⌘,) re-press now reverts live-applied
    edits like Esc does, instead of silently keeping partial
    changes.

Multi-window persistence (per-window override propagation)

Two runtime bugs in the per-window theme + cfg override
persistence layer closed. The on-disk [[windows]] Vec format
has worked correctly at save / restore time since v3
(acb275c); the leftover gaps were at:

  • spawn_in_process_window (⌘N / Window > New Window). Was
    reading the global cfg.last_window_theme / cfg.last_window_cfg
    slots, which are last-write-wins — set by whichever window
    most recently committed an override via Settings's window-scope
    save. Spawning a new window would silently "rewind" to that
    previous override instead of inheriting from the window the
    user was looking at. Now: ⌘N inherits the focused window's
    in-memory theme_name + cfg_override ("duplicate this
    window's setup"). Initial-window restore on app launch still
    reads from the per-window window_state.toml entry as before.
  • Reset Window Settings (tmnl > Reset Window Settings) and
    Apply Window Settings to All Windows cleared the in-memory
    override but didn't write the change to disk. Next launch's
    restore read the still-populated cfg_override_toml in
    window_state.toml and resurrected the override. Both menu
    items now call save_window_state after the clear.

Theme picker discoverability

  • View > Theme submenu lists every discovered theme +
    (global) sentinel; pick to apply as the focused window's
    override. Mirrors the ⌘⌥] / ⌘⌥[ cycle chord with a
    discoverable surface.
  • New :theme.cycle_next / :theme.cycle_prev colon-prefix
    launcher-chip sentinels. Add via the + Add integration
    overlay's Built-in actions category to get a one-click
    theme cycle chip in the launcher rail / top strip / bottom
    strip (wherever the user pinned it).

Inline images (protocol v7)

Three new client→server messages let backing apps overlay PNG
images on the cell grid:

  • InlineImageCreate — anchors a raster image at a cell
    coordinate inside the pane (anchor stays put as the pane
    scrolls). Client-allocated id identifies the image for the
    later Update / Destroy. The cell_w × cell_h box scales the
    decoded PNG to fit the requested cells.
  • InlineImageUpdate — replaces the encoded bytes without
    moving the anchor; useful for animation or live data.
  • InlineImageDestroy — removes the image. Dropping a pane
    also drops its images.

Tmnl decodes once (image crate, PNG-only for now — the
ImageFormat enum reserves room for more formats), uploads to a
wgpu texture, and renders via a textured-quad render pass. The
quad alpha-blends over whatever the cell grid painted underneath,
so transparent pixels show the cell content through. Decode
failures paint a dim-red placeholder with a nf-md-image_broken
glyph at the image's anchor rect — the host doesn't crash on bad
input.

Gated by Caps::INLINE_IMAGES. Pre-v7 servers ignore the
messages; v7 clients should gate sends on
peer_caps().contains(Caps::INLINE_IMAGES).

SDK helpers:
Client::send_inline_image_create / _update / _destroy. End-
to-end demo in examples/inline_image_client.rs — encodes a
gradient PNG in memory and walks the Create / Update / Destroy
lifecycle. Manual page: SDK — Inline images.

GPU cache survives a tab switch (each image gets a stable
gpu_key assigned at creation; the cache keys on that, so the
same image displayed in two tabs costs one texture upload).
Per-frame upload list shares the decoded RGBA buffer via Arc
no per-frame memcpy of pixels.

A pane resize / split that shrinks the pane below the image's
anchored cell box clips the textured-quad to the pane edge with
matching UV adjustment (the visible region samples the
corresponding texture sub-rect — no stretch). Anchors entirely
off the pane skip the draw without leaking into chrome.

Cross-platform global hotkey

  • macOS now uses Carbon's RegisterEventHotKey (657470e) which
    requires NO Accessibility permission — the prior implementation
    was Accessibility-gated and quietly no-op'd if the user had not
    granted it. Linux + Windows use the global-hotkey crate
    (16977e5). Configurable via the quick_terminal_hotkey config
    knob; pressed → spawn a sibling tmnl --quick-terminal.

Linux first-class support

  • Notification chime via notify-rust (c305d69 + 56f6009):
    shell BELs surface as native dock notifications on GNOME / KDE
    with a configurable sound (bell_sound). Matches the macOS dock-
    badge story.
  • notify_command (56f6009) — opt-in shell-out to a custom
    notify program (e.g. dunstify, terminal-notifier) when the
    built-in chime isn't enough.
  • Install / launch / known-limitations docs (544d37d) catch up
    the Manual.

Headless commands + VHS tape recorder

  • New snapshot <path> headless command (605406e) renders the
    current App state to an offscreen wgpu texture and writes a PNG
    — works without a display, captures chrome correctly.
  • New tmnl --tape <path> mode (87750d1) runs VHS-syntax scripts
    through the headless dispatcher. Same .tape file format mnml +
    mixr already use.
  • Output <path.gif> directive (bcc9f41) composes the per-frame
    Screenshots into an animated GIF via gifski. Set FrameRate N
    controls the FPS.
  • Set DemoChrome on (9939dff) paints fake macOS traffic-light
    discs at the conventional top-left position via 3 SDF instances
    on the strip pipeline — sells the snapshot as a real window.
    AppKit paints over them on live runs, so they only matter for
    offscreen captures.
  • Strip-pipeline blend state migrated to ALPHA_BLENDING so the
    SDF circle edges anti-alias correctly (4825ea9).

Benchmark framework

  • New tmnl --bench <scenario> subcommand (ee173cd, 0c5cc39)
    measures input latency and cross-terminal comparisons. See
    docs/latency-bench.md and the Manual's Performance page.
  • examples/latency_glyph.rs (e8606fd) is a glyph-render test
    app for sub-frame timing validation.

Install tmnl-rs 0.2.0

Install prebuilt binaries via shell script

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/chris-mclennan/tmnl/releases/download/v0.2.0/tmnl-rs-installer.sh | sh

Install prebuilt binaries via powershell script

powershell -ExecutionPolicy Bypass -c "irm https://github.com/chris-mclennan/tmnl/releases/download/v0.2.0/tmnl-rs-installer.ps1 | iex"

Download tmnl-rs 0.2.0

File Platform Checksum
tmnl-rs-aarch64-apple-darwin.tar.xz Apple Silicon macOS checksum
tmnl-rs-aarch64-apple-darwin.pkg Apple Silicon macOS checksum
tmnl-rs-x86_64-apple-darwin.tar.xz Intel macOS checksum
tmnl-rs-x86_64-apple-darwin.pkg Intel macOS checksum
tmnl-rs-aarch64-pc-windows-msvc.zip ARM64 Windows checksum
tmnl-rs-aarch64-pc-windows-msvc.msi ARM64 Windows checksum
tmnl-rs-x86_64-pc-windows-msvc.zip x64 Windows checksum
tmnl-rs-x86_64-pc-windows-msvc.msi x64 Windows checksum
tmnl-rs-aarch64-unknown-linux-gnu.tar.xz ARM64 Linux checksum
tmnl-rs-x86_64-unknown-linux-gnu.tar.xz x64 Linux checksum