Everything since v0.7.0
Sessions that survive a daemon restart, one pane geometry the daemon negotiates for every client, an interface that answers the mouse, a file browser in the rail, agents that can message each other, a screenshot command, and a read-only view of other machines.
Gaurav Gosain
1,708 commits since v0.7.0, across 1,412 files. This page has been written in three rounds. The newest work is at the top, the round from late August is below it, and the round from July is below that. Both of those are unchanged and still hold. The short version of the new work: your sessions come back after the daemon restarts, the daemon decides one pane geometry for every client instead of letting each client guess, the whole interface answers the mouse, agents in separate panes can talk to each other, and there is now a screenshot command, a screen saver, a file browser in the rail, and a read-only view of the sessions on your other machines.
Your sessions come back
The daemon saves its sessions under $XDG_STATE_HOME/tuios/sessions and
restores them when it starts. That is on by default. tuios daemon --no-restore turns it off, and tuios resurrect lists what is saved or
brings one session back by name.
Each window comes back with its workspace, its place in the layout, its name and its working directory, and the session comes back with its display name and accent. The shell in each window is a new shell, started in the old directory. Scrollback is not saved, and neither are the programs that were running. A window whose shell will not start is dropped rather than restored empty. The state file carries a version, and a file tuios cannot read is moved aside for two weeks instead of stopping the daemon.
One shape for every client
Two clients on one session used to disagree about how big a pane is. Each client subtracted its own chrome, its own rail and dock, from the screen and retiled, and the two answers fought over the same pseudo-terminals. A client now reports only how much it reserves on each edge. The daemon takes the largest reserve on each edge across every attached client and hands back one geometry. A client with less chrome than the agreed figure draws a blank band rather than moving the panes.
The handshake carries a protocol version, and the version is now 3. Version
2 is refused. That is not a cosmetic bump. A message type had been inserted
into the middle of the list after v0.7.0 without a version change, which
shifted the type byte of every message the daemon sends. An old daemon and a
new client still connected, and each read the other's replies as a different
message rather than as an error. When the two disagree now, the message says
which side is newer and names tuios kill-server. The JSON verb protocol
carries its own separate version, still 1.
Switching workspaces no longer resends scrollback the client already holds. The request says how many rows the client has and the daemon sends the rows past that. A cold attach still gets up to 1,000 rows per pane.
Session display names and accent colours moved into the daemon. They used to live in a per-client file, so a second client attached to the same session never saw a rename, and neither the name nor the colour survived a reattach.
The daemon speaks 46 verbs now, over a line-delimited JSON socket: one JSON object per line in, one per line out.
Driven by the mouse
Every overlay row highlights under the pointer and acts on a click: the settings panel, the command palette, the launcher, the theme, glyph and effect pickers, the session and workspace lists, the layout menu and the dialogs. Two exceptions are deliberate. The keybind manager's rows select but do not fire, because inspecting a binding is not running it, and the help panel only scrolls.
Right-click opens a menu for whatever is under the pointer: a pane, a dock segment, a workspace pill, the dock, the desktop, or a session row in the rail. Every row in those menus is an action from the keybind registry, and the key it shows comes from the registry too, so the hint cannot drift from the binding.
Alt and the left button move a pane from anywhere in its content, and alt and the right button resize it, so you no longer have to find a border first. The plain left button selects text, with a double click for a word and a triple click for a line. A plain right-click over a shell opens the pane menu; over a program that has asked for the mouse, the program keeps the right button and the menu needs ctrl or shift.
The rail answers the mouse throughout. Rows highlight on hover, a truncated row scrolls so you can read it, the collapsed strip pops a label beside the row under the pointer, session rows can be dragged into an order, and the rail's own edge is a one-cell target for dragging its width.
The dock lost its powerline caps and reads as a status line. Its contents
are a [dock] table: left, center and right are ordered lists of
component names, [dock.clock] sets the time format, and a
[dock.custom.NAME] entry runs a shell command and shows what it prints.
Workspace pills can be dragged into a display order, which does not renumber
the workspaces.
The keybind manager gained the thing it was missing: a way to give a key
back. ctrl+x on a row, or tuios keybinds free <key>, strips that key off
every action in every scope and writes the empty list down, so the program
in the pane receives it. tuios keybinds unbind is the narrower request,
taking one action off one key.
The rail lists your files
The rail has a files section. It lists the working directory of the focused
pane and follows the pane as it moves. Entries carry nerd font icons, and
each icon takes its own file type's colour, corrected toward the rail's
background so it clears a 3:1 contrast floor instead of vanishing into it.
appearance.sidebar.file_icon_colors turns the colour off. Clicking a
folder navigates the listing, sends a cd to the pane, or both, set by
appearance.sidebar.folder_click.
Six file actions: create, rename, delete, copy, cut and paste. Only delete
asks first, and the dialog names the file and says whether it can be
recovered. A delete moves the file to $XDG_DATA_HOME/Trash following the
freedesktop.org spec, writing the .trashinfo record itself rather than
shelling out to another tool. appearance.sidebar.file_delete = "permanent"
skips the trash. It is home trash only: a file on another filesystem is
refused rather than quietly destroyed, and Windows has no trash at all, so a
delete there is permanent and says so. Paste never overwrites; a name that
already exists gains a number.
Which sections the rail draws, in what order, and how much height each one
takes, is one string, appearance.sidebar.sections, with an editor for it
in the settings panel under Sidebar. spacer is the one name that may
repeat, because a gap is not a section and a layout may want several.
Agents can talk to each other
An agent in one pane can find the others and leave them mail.
tuios list-agents names every pane running an agent and its state,
tuios send-agent-message drops a message in another pane's mailbox,
tuios read-agent-messages collects your own, and tuios ask-agent sends a
question and waits for the answer.
The bounds are the point. A message is at most 8KB, with a 120 character
subject and eight attachments, which travel as paths and never as bytes. A
session's mailbox holds 256 messages or 512KB, whichever comes first. A
sender gets a burst of ten, and thirty messages per minute. Mail lives in
memory and dies with the daemon. And ask-agent refuses a question that
would deadlock: not only asking yourself, but any question that would close
a cycle of agents already waiting on each other. It tells you to send a
message instead.
Detection reads 22 agent CLIs from TOML manifests now, and per-pane state comes from six ranked sources rather than four. Below the agent's own report, its transcript, escape codes and screen heuristics sit two more: what the foreground process is, and, last of all, an output stall.
tuios update replaces the binary in place, and refuses when it did not
install it. It knows six origins: a release download, the install script,
go install, a Nix store path, Homebrew, and a system package. Only the
first is safe to overwrite. For the others it names the installer that owns
the binary and what would go stale if it wrote over it.
What the oracles found
The conformance rounds described further down fixed more than that section named, and the fuzzing around them grew two oracles that need no expected output at all. The first says a frame the emulator emits must redraw the screen the emulator holds. The second says the same bytes must draw the same screen however the read boundaries fall between them. Neither needs a reference terminal, and between them they found two root causes: a wide rune severed at a margin left half of itself behind, and zero-width input was given a cell of its own instead of a base to attach to. A third fuzzer feeds identical bytes to the pure-Go emulator and the ghostty one and compares; it catalogued eight places where the two disagree, now pinned as known divergences rather than quietly passing.
Some of what those rounds fixed is worth naming, because any program in a pane could reach it. DECSTBM, which sets the scroll region, stored the bottom row a program asked for without checking it, so a region sized for a taller terminal survived into a shorter one and the next scroll indexed past the screen. That panic lands in the PTY reader, which takes the daemon down and every session with it. OSC 8 had its two fields the wrong way round, so every hyperlink a program emitted carried the parameters where the URL belonged and did not work. DECSTR, the soft reset, was not implemented at all. Insert mode was recorded, reported as set when a program asked, and read by nothing, so a program that switched to insert mode overwrote the line it meant to open a hole in. And DECRQSS, which asks the terminal to report a setting back, was ignored rather than answered.
Three tilers, one contract
A pane can be floated out of a tiled layout on its own, and pinned so it follows you across workspaces. The scrolling layout, in the style of niri, puts columns on an endless horizontal strip. Master-stack is a third engine beside BSP and scrolling. "All windows" in the command palette shows every window in every workspace at once, searchable, and jumps to the one you pick.
Two of the three tilers let tiles overlap instead of shrinking. BSP was
fixed in July. Master-stack had the same defect and was fixed in August: at
45 by 14 cells it overlapped from five panes up. The same change made all
three honour appearance.gap, which master-stack applied on one side only
and the scrolling layout ignored entirely.
A picture of a pane
tuios screenshot writes a pane to a file as png, svg, ansi, html
or txt. It runs in the daemon and reads the emulator's resolved cell grid
rather than re-parsing an ANSI string, so indexed colours keep their
identity and the picture has the shape of the terminal it came from. Because
the daemon owns the pane, tuios screenshot -w build answers on a detached
session, from a script, with nobody attached.
Kitty graphics work over SSH now. Passthrough used to be decided by asking
the terminal on the server what it could do. A server has no terminal, so
the answer was no, and graphics were off for everyone who connected to it.
The client's capabilities are now read from what the SSH connection already
carries: the TERM in the pty request, the forwarded environment, and the
pixel dimensions the client reports, which also give the cell size.
Links, and a screen saver
appearance.links decides what tuios treats as a link in a pane: off,
marked for the ones a program declares with OSC 8, or all, the default,
which adds bare URLs found in plain text. The link under the pointer is
underlined, the pointer turns into a hand, a label says where it goes, and a
click opens it. A file:// link opens on whichever machine runs the client.
An http link opens a browser only when the client is local; under
tuios ssh or tuios-web it copies the URL to your clipboard instead,
because opening a browser on the server would help nobody.
The screen saver draws over the screen after screensaver.idle_minutes of
nothing happening, and is off until you set screensaver.enabled. There are
36 effects, ported to Go from terminaltexteffects and now kept in their own
library, and the picker animates the one you are on over a capture of your
real screen rather than over sample text. screensaver.effect picks one, or
leaves it random.
Every option in one list
There is one registry of settable options, 121 of them. It backs
set-option, get-option and list-options on the socket, and the
settings panel is derived from it: a test fails if a registry path has no
row in the panel, apart from a short list of exclusions, each carrying its
reason.
One bug it exposed: a run of settings applied only on the path cmd/tuios
takes. Border style, dock position, window buttons, the scrollbar, the three
dock meters, scrollback lines, reverse scroll, the frame cap, the theme and
the leader key were written to the config file correctly
by the settings page and then read back as defaults on every config reload,
and were never applied at all under tuios tape or the embed API. Boolean
toggles had a second fault: they were applied only when true, so turning one
off could not be saved. Applying a config file is one function now, and a
test walks the whole file to prove it.
A theme can be described as well as applied. The description measures the foreground, the cursor and all sixteen ANSI colours against the theme's own background and reports which of them fall under a contrast floor, so an illegible swatch is named rather than shipped.
A theme is only half a rice. appearance.glyphs picks a glyph set, which
owns the shapes: border corners and junctions, window controls, the rail's
marks, folder glyphs. Four sets ship, default, unicode, heavy and
ascii, each glyph carrying its own ASCII fallback rather than one fallback
for the whole set, and a custom set is a file that can inherit from a
built-in. tuios list-glyphs lists them, and the settings panel has a
picker with a preview.
Other machines, listings only
tuios can look at the sessions on your other machines. Add a
[hosts.build] table with an addr of user@machine, then tuios hosts
lists the machines, tuios ls --all-hosts lists their sessions, and
tuios list-agents --all-hosts lists the agents running in them. The rail
draws them as muted rows.
That is all of it. Nothing crosses a link except listings. You cannot start a session on another machine, attach to one, resize it, type into it, kill it, or rename it, and a remote row in the rail cannot be clicked, dragged, renamed or switched to. This is the first release of the feature and the boundary is deliberate. The CLI help says the same thing.
The transport is one ssh child process per host, running
tuios stdio-proxy on the far side with BatchMode=yes, so it never
prompts: you need to be able to ssh to the machine without typing
anything first. The proxy
bridges to that machine's daemon socket and deliberately does not start one,
because starting a daemon would restore that machine's sessions, and that is
a change, not a listing. A host that is down costs its own row and nothing
else.
Faster underneath, measured
Every number here is from the commit that made the change.
- Rehydrating a workspace switch where the client is already caught up: 2,878,917 bytes, 316,544 allocations and 47.6ms per pane, down to 146,598 bytes, 10,775 allocations and 2.4ms.
- A keystroke over a real unix socket: 4,009ns and four writes, down to 967ns and one write.
- Input latency, after a leading-edge coalescer replaced a free-running ticker: the coalescer's own median went from 5.01ms to 20.1us, and the median round trip from keypress to echo from 9.75ms to 1.78ms.
- An idle daemon: 470ns, 568 bytes and 9 allocations per tick, with one unit of work per tick, down to 260ns, 296 bytes, 5 allocations and no work at all.
- Drawing a pane's frame, after tuios stopped asking a layout library to measure a box whose size it already knew: 6.53ms to 1.58ms when the pane is dirty, 3.51ms to 62.7us when the content is cached.
- The rail's rows, cached behind a signature of what they depend on: 82,000ns and 178 allocations per frame, down to 288ns and none.
Six changes to your setup
Most of these need nothing from you.
- The wire protocol is version 3, and version 2 is refused. If a daemon and
a client from different builds meet, the message says which is newer. Run
tuios kill-serverand start again. - Digits 1 to 4 in window mode select windows. They used to snap a window to a corner. Corner snap moved to the layout prefix. An old config still carrying the default corner-snap bindings is migrated on load; one you changed yourself is left alone.
- Right-click on pane content opens the pane menu when the pane is running a shell. It used to only resize. Over a program that has asked for the mouse, the program still gets the right button and the menu is on ctrl or shift with right-click. Right-drag still resizes.
appearance.separator_styleis gone. It parsed and nothing read it.appearance.gapwas removed at the same time for the same reason, and has since come back as an option all three tilers honour.- Sidebar settings moved from flat
appearance.sidebar_*keys into an[appearance.sidebar]table. The old keys are folded in on load, the table wins where both are set, and the file is not rewritten in the old spelling. - The
colorshiftscreen saver effect is gone. A config that names it reads asrandomrather than failing validation.wavesandvhstapearrived in the same change.
Everything below is the round of this page written in late August, unchanged because it still holds. Two counts in it have moved since: the daemon speaks 46 verbs, not 35, and agent detection reads 22 manifests, not nine.
The sidebar knows what your agents are doing
The rail was rebuilt from a collapsible session tree into three flat
sections: sessions, terminals, and agents. The agents section is the point.
Every pane running a recognised coding agent shows up there with its state,
ordered by who needs you: errored first, then waiting for input, then
finished but unseen, then working. A finished agent you have looked at decays
down the list instead of staying green forever. Rows carry the harness name
and the last thing the agent said it was doing, and the command palette can
filter panes by state, @n for needs input, @w for working, across every
session at once.
Detection is data, not code: nine harnesses ship as TOML manifests, matched
against what the foreground process actually is rather than what it is
named, and tuios explain-agent-detect shows the reasoning for a pane when
it guesses wrong. State comes from ranked sources rather than one flag: an
agent's own report outranks its transcript, which outranks escape codes,
which outrank screen heuristics, and a lower tier can never overwrite a
higher one that is still fresh. Where a harness keeps a session log, the
daemon reads the agent's own record of its turn; the reader looks at the
last 128KB and no further, so a 151MB transcript costs 652 microseconds.
When a state changes, tuios can tell you about it: a dock message, an OSC 9
notification that reaches you over SSH because it travels in-band, and an
optional sound cue, all governed by a [notifications.agent] table with
per-state switches, a settle window so flapping states do not spam, and
quiet hours. An after-agent-state hook hands the transition to any script,
through environment variables rather than argv so a harness's free text
cannot become shell arguments.
Around the agents, the rail grew the rest of what it was missing: every session gets a colour of its own, derived from its name so it is stable across clients and restarts; sessions and workspaces can be renamed from one dialog; the collapsed strip mirrors all three sections; and the dock's workspace pills can be dragged into an order, display order only, because a workspace's number stays its identity.
A second emulator, and the suite that stands between two
Behind a ghostty build tag, panes can be driven by libghostty-vt, the
emulation core from the ghostty terminal, instead of the pure-Go emulator.
The Terminal interface the rest of the app consumes was extracted first,
so both backends implement one contract, and a differential suite feeds
identical bytes to both and compares screens, cursor, scrollback, and modes,
now after every chunk and on the rendered output a host would receive, not
just on internal state at the end of input. That "now" is load-bearing: the
suite as first written passed while the ghostty backend painted ls on hot
pink, and the post about why
is the most instructive bug of the batch. The suite runs in CI on every
change that touches the emulator, and releases ship ghostty-tagged artifacts
alongside the default ones.
A control surface a program can trust
The daemon speaks 35 verbs over its socket now, wrapped in CLI commands, and
the batch that added the arrangement and settings verbs also changed what
the existing ones were allowed to say. A parameter a verb does not take is
refused with the closest spelling and the full accepted list, instead of
being silently dropped by the JSON decoder. set-option validates the path
against a registry of every settable option and returns option_not_found
with suggestions for a path that does not exist, instead of recording
nonsense and reporting success. The registry is held to the config struct by
a reflection test that fails in both directions, and applying a setting at
runtime goes through the same funnel a config file does, which retired a
hand-written switch that supported exactly six paths out of 88. Colour-valued
options validate their values too.
The protocol carries a wire version in both directions of the handshake, and
each side refuses a peer outside its range with a message that says which
side is newer. An agent skill ships in the binary, printed by
tuios --skill, and every command example in it is parsed against the real
command tree by a test, so the documentation cannot drift from the binary.
Most of this hardening came from driving the socket the way a machine would
and watching what actually happened, which
got its own post.
Run anything
tuios can now launch programs: anything on $PATH, fuzzy-matched with
frecency ranking. This started inside the command palette and then earned
its own overlay, on alt+space, because a palette row is a verb tuios
performs and a launcher row is a thing you start, and ranking the two
against each other meant a handicap constant whose whole job was to stop
"new" from offering newgrp above New Window. The launcher shows names
and icons from desktop entries where the platform has them; Enter execs
the selection directly in a new pane, so what you launched is what runs,
with no shell history entry and no prompt flash, and Tab types the command
into the focused shell instead for when you want to add arguments.
Underneath both, the four separate search boxes in the app were collapsed
onto one matcher that scores instead of shrugging, with its invariants held
by fuzz tests on random input.
Your keys, inspected
Every key tuios answers to now lives in the keybind registry, including
the two that used to be literals in the input path: the palette on
ctrl+p and the launcher on alt+space sit in a [keybindings.global]
section consulted in every mode, because a key nobody can rebind is a key
nobody can inspect. On top of that registry sits a keybind manager, on
ctrl+b k: four tabs over one analysis, what is bound, where tuios argues
with itself, where tuios argues with the pane's program, and a Record tab
that captures the next key you press and tells you all three about it. The
same analysis runs headlessly as tuios keybinds doctor and
tuios keybinds explain <key>, and every finding says how it knows:
certain from tuios routing, observed read from a live pane,
reference from a curated list of program defaults, so a claim about what
nvim wants never masquerades as detection.
Bring your colours with you
tuios import-theme converts a kitty, ghostty, alacritty, or wezterm
colour scheme into a tuios theme, sniffing the format from the file's
content, and tuios list-themes lists everything the daemon can apply,
342 built-ins and whatever the themes directory holds, re-read on every
call so an imported theme is usable without a restart. Switching re-palettes
every open pane in place.
tuios in your pocket
tuios-web can serve a LAN address over TLS, and --auto-tls generates and
keeps a certificate covering the hostname and every interface address, with
tuios-web cert subcommands to inspect or replace it. On a phone, the web
client shows a collapsible row of tuios commands nearest the thumb, built
from the keybind registry so a rebound command moves its button; a long
press opens the pane menu, and pane dividers get a touch target big enough
to grab. Settings changed from a served browser session apply to that
session only, instead of writing the operator's config file.
Animated graphics, without the flood
The kitty animation protocol, tried and dropped in v0.7.0, is implemented: frame transmission, composition, and control, covered end to end by tests. Animated guests stream damage patches instead of whole bitmaps, which took one measured page from 330MB forwarded in 21.6 seconds at 17fps to 22MB at 73fps with input still responsive. A graphics flood is paced so it cannot starve the keyboard, every frame of a stream gets placed instead of just the first, a placement hold during a pane resize releases when the geometry settles rather than waiting for a mouse release that may never arrive, and shared-memory transmissions are unlinked after reading so they do not accumulate.
Colour, chrome, and zen
Colour options in the settings menu open a real picker: a hue strip, a shades grid, saturation and lightness sliders, RGB sliders, a typeable hex field, a harmony wheel, and the theme's own colours as a first row of slots. A slot re-resolves when the theme changes; a picked colour stays what was picked. Session accents, window accents, border colours, and the scrollbar tint all route through it, and settings rows show a swatch of the colour in force, so an unset row shows what it inherits.
appearance.zen_mode hides unfocused window borders, always or only while
the mouse is still; the focused frame stays as an anchor, and the melt is a
repaint, not a layout change, so content never shifts. Window buttons can
sit on the left, and a dots style draws macOS-like traffic lights, with
their colours nudged toward the pane's text colour where they would miss a
3:1 contrast floor. The scrollbar got two deliberate styles, a single stroke
at two weights in the pane's own ink, or a track with a half-cell thumb
borrowed from opentui, and its default tint is measured against the pane
background rather than hoping the border colour is visible.
Held to what real terminals assert
Two conformance rounds, both built on a corpus in the style of ghostty's terminal tests: a byte string into a 6x4 screen compared against a literal text dump, because on 6x4 the diff is the bug. The corpus cites esctest and vttest per test, a differential mode compares against a real tmux, and a grammar-based generator fuzzes the emulator with the sequences that have historically broken terminals, which found two cursor-clamping bugs in its first six seeds.
Round one found sequences that were simply missing: NEL, which terminfo uses
to move down a line; DECALN, the alignment pattern vttest opens with, so a
terminal that ignores it looks like it passed everything that follows; and
DECOM homing. Round two produced eleven fixes, among them a cursor position
report that had line and column transposed, so every program that asked
where the cursor was got the answer backwards; SO and SI registered where
the parser could not reach them, so under TERM=screen, linux, or vt100
every box drawn through terminfo came out as lqk instead of line art; ED 3
clearing the visible screen along with the saved lines; REP repeating the
last ASCII byte instead of the last grapheme cluster; and a mode map read
without its lock, which in Go is a runtime throw rather than a recoverable
panic, inside the daemon that hosts every session.
Faster underneath
Idle first: with idle shells and the sidebar on, tuios now schedules zero timer-driven renders, the maintenance tick skips its scans when nothing moved, and the session poll only runs while something on screen consumes it. Startup asks the terminal everything in one round trip and stops reading at the DA1 reply, which took exec-to-first-frame from 566ms to 68ms on any terminal that answers. Switching workspaces stopped resending scrollback the client already had, which at four panes turns 11.5MB of daemon encode into 0.6MB.
Under load, the render coalescer paces each pane by what its frames actually cost instead of asking for one every 8ms, and a pane that falls more than 4MiB behind drops to catch-up rate until it recovers; typing into a pane while another one floods went from a 18 to 28ms median round trip to one to five, against tmux's one. The pane renderer stopped word-wrapping content the emulator had already laid out to exactly the pane's width, which is a fifth of every frame spent changing nothing. And the frames tuios sends the host are bracketed in synchronized output with a hold that survives invalidation, with an e2e harness that now answers the terminal queries bubbletea asks, because until it did, the entire suite had been running in the one configuration real terminals never present.
The daemon, when you want it
startup.daemon = true makes a bare tuios start or join the daemon and
restore every saved session, off by default so nothing changes for anyone
who did not ask. tuios ls with no daemon running now lists the sessions
saved on disk, marked as saved, and exits 3, so a script can finally tell a
stopped daemon from a running one holding nothing. And tuios attach starts
the daemon instead of refusing and pointing you at new; your sessions were
one process away the whole time.
Everything below is the earlier round of this page, from July, unchanged because it still holds.
Project tapes
A .tuios.tape is a file in a project directory that describes the session for
that project: which windows to open, what to run in them, how to lay them out.
When the focused shell cd's into a directory that carries one, tuios notices.
Like direnv, but for terminal layouts.
A tape is arbitrary command execution, so the whole feature is built around a trust boundary. A tape is inert until you have read it and chosen what to do with it: Run once, Trust and run, or Never. Trust binds to the file's content hash, so any edit reverts the tape to untrusted and it prompts again. By default nothing opens on its own. A badge in the dock and a notification tell you a tape is there, and the review dialog shows the full content before anything executes.
Two opt-ins trade friction for convenience without weakening the boundary.
auto_review opens the review dialog on detection instead of waiting for a
keypress. autorun = "auto" runs a tape without asking, but only one you have
already trusted and not edited since; an untrusted or changed tape falls back
to asking. Running a tape builds a session named after the project and switches
you into it.
Start where you want to be
Every session used to start empty: a blank floating workspace, then the same
three actions every time. Open a terminal, turn on tiling, enter terminal mode.
Those are now three startup settings, all off by default so a fresh install
behaves exactly as before: open_default_window, tiled, and
start_in_terminal_mode. All three on means tuios launches into a tiled
session with one terminal and the next keystroke goes to the shell.
The settings menu now covers everything the config file does. Border colors, preferred shell, window title format, daemon log level, mouse scroll speed, and the rest of what used to be file-only are in the menu and the command palette, with an inline text editor for free-text values. Changes persist. And when the config file itself has a problem, the error now surfaces inside the running TUI instead of being silently ignored.
open_default_window for a floating terminal, add tiled to fill the tile, and start_in_terminal_mode to put the cursor in the shell, where the mode reads TERMINAL and the cursor is solid rather than hollow.The web client can shape text now
The web client moved to sip's WebGL renderer with real HarfBuzz text shaping, opt-in. A browser terminal normally draws one glyph per codepoint, which for Arabic means isolated, disconnected, wrongly ordered letters. With shaping, letters join, ligatures form, and combining marks land where they belong. This is the difference between a terminal that technically displays Arabic and one that can be read.
The same sip work fixed a class of freeze where a terminal stopped repainting under fast output (synchronized output, mode 2026, left engaged), matches grapheme cluster widths against a reference terminal instead of a guess, and corrects kitty graphics placement.
Tiling, resize, and staying responsive
Windows created in a daemon session now tile. Placement used to only run for windows the attached client created itself, so daemon-created windows stacked on top of each other at the same position or floated over the tiled ones. A sync that re-places a window in a tiled session now retiles, and BSP tiles no longer overlap when panes shrink past the default size.
Resize was rebuilt after one bug report unravelled into three distinct defects, which got its own post. The short form: dragging a shared border now moves the two panes that share it and nothing else, an untouched split's ratio no longer drifts every time something near it resizes, and the drag sets geometry directly instead of easing toward a target that has already moved. Both defects are demonstrated interactively in the post.
A busy pane can also no longer starve the rest of the app. A background pane flooding output used to hold the emulator lock for its entire pending batch, and the renderer, which needs that lock every frame, never got in. Output is now written in bounded chunks, so the renderer gets its turn and typing stays responsive no matter what another pane is doing. The August pacing work above built on top of this.
Keycast
tuios can now draw your keypresses on screen as you type: a small overlay in the bottom right showing the last few keys, with repeats coalesced. I added it for screen recordings, where a keyboard-driven window manager is unreadable without one. It toggles live from the command palette and the setting persists.
Fewer papercuts
- Ctrl+P opens the command palette every time. It was silently swallowed when the terminal reported a lock modifier like Num Lock in the key event. Keybindings now resolve through a single registry, so a binding means the same thing everywhere.
- Quitting a session kills it, and the message says "Killed" instead of "detached". The exit message names the session you were in, not the one you first attached to.
- A grapheme cluster split across two writes no longer renders as two. Write boundaries fall anywhere; the emulator now treats that as its problem.
- Erase-character respects the right margin.
- Programs running inside tuios are told graphics forwarding is available, so image-capable tools stop falling back to blocks.
- Mouse scroll speed and window title format are configurable, in the file and in the menu.
All of this is on main now and will ship as the next tagged release. If a tape prompt ever surprises you, that is the trust boundary doing its job.