The Rust package and executable are named hmux. The library target retains
its tmux_c2rs name for the transpilation's existing consumers.
The implementation began as a whole-program
c2rust transpilation of tmux 3.7b. It now
combines translated command behavior with Rust-owned engine components and the
hmux-rt runtime. The end-user compatibility contract is the tmux-compatible
command line, wire protocol, and observable server behavior, with the deliberate
differences documented below.
new-pane and split-window accept -T title, following upstream tmux commit
df7c2e60. The title expands formats against the command's original target and
uses the existing trusted command-title sanitizer. Like portable tmux 3.7b,
hmux accepts an empty title and preserves #; the OpenBSD snapshot at this
commit rejects empty titles and replaces # with _. The title-change
notification is emitted whenever -T is supplied, including for an empty title.
Format evaluation follows upstream e880cf63 for session/window/pane/client
loop context and scalar modifiers. loop_index is zero-based in the selected
sort order; loop_last_flag belongs to the innermost loop. Both are empty
outside loops, and completing an inner loop restores the outer context. Pane
loops support index (P/i) and z-order (P/z) sorting.
t/d reports signed seconds since a timestamp. q/s uses POSIX single quotes;
q also escapes braces, tabs and newlines. c/f and c/b produce foreground
and background SGR sequences, with none resetting attributes. These cover
existing and symbolic theme colour names, resolved against the selected client palette.
The pinned target emits indexed and RGB SGR sequences even for a vt100
client; this behavior is characterized, rather than claimed as colour fallback.
Server option theme selects detect, terminal, light or dark.
The ten symbolic colours (themeblack, themewhite, themelightgrey,
themedarkgrey, themegreen, themeyellow, themered, themeblue,
themecyan, thememagenta) use the corresponding dark-theme-* or
light-theme-* option. These accept formats, including client_colours.
Terminal mode uses ANSI colours; detection prefers the terminal's reported
theme and falls back to its background. Changes invalidate client colours
and redraw without waiting for the status interval.
Cursor, prompt cursor and clock colours are format-valued strings. Empty cursor colours select the terminal default; invalid literal updates are rejected without changing the stored value. Unlike the 3.7b numeric colour options, these strings retain the supplied spelling when shown. Pane theme queries prefer an attached terminal's report over a pane-background guess; conflicting or absent reports retain the background fallback.
m/z matches fuzzy queries and m/p returns the matched display columns.
Space-separated terms are ANDed, | separates alternatives, ' requests an
exact substring, ^/$ anchor a term, and ! excludes exact matches. Matching
folds ASCII case unless the query contains uppercase ASCII. Styles occupy no
columns; wide characters highlight both cells. One shared Rust matcher uses
the existing style and UTF-8 components and returns scores for future mode
callers. The existing POSIX regex component continues to handle m/r and
substitutions. A start-anchored substitution replaces its first match even
when empty, preserving the first input character (#{s/^/X/:...}).
src/ is the source of record and is edited directly; the transpiling pipeline
has been retired. Command, server, screen, grid, input, and other subsystems have
their own module directories. Shared entity and compatibility types remain in
src/types.rs, with private window storage in src/window.rs, private pane
storage in src/window_pane.rs, and session state in src/session.rs. External C declarations
are centralized in src/ffi.rs; Rust subsystem calls use their actual Rust
signatures.
Ownership migration is ongoing. Options and several callback-state families use
Rc<RefCell<_>> handles and checked borrow guards. Session and client owners
no longer implicitly dereference to payload references; some compatibility
access still requires explicit unsafe calls. Window and pane payloads are
private to their owning modules and have no raw-pointer or whole-payload
borrowing interface. Each pane's window holds
a strong RustWindowPaneRef, moved between windows during transfer.
RustWindowPaneWeak is the non-owning observation used by command targets,
modes, callbacks, and lookup results. Cloning an observation does not extend
the pane's lifetime; explicitly upgrading it produces a strong reference.
The global pane index stores weak observations without traversing a window's pane list.
Pane moves and swaps preserve allocation identity and update a weak window
back-reference. Stream callbacks use weak pane observations and skip removed
or temporarily detached panes.
Pane destruction unregisters the pane at the equivalent point to tmux 3.7b's
RB_REMOVE, marks the allocation destroyed, and tears down its resources once.
Outstanding observations then return None. Event payload values and retained
event targets keep the allocation available until their final strong reference
is released, while ordinary observations remain weak. Detached panes remain
registered during transfer, and membership-specific lookup checks their current
weak window reference. Pane IDs are never reused during a server's lifetime.
Chooser modes accept -k to kill their pane when the mode exits, following
upstream tmux commit 34fd261a. The kill runs synchronously when the outermost
pane operation releases its temporary owner. Destruction itself only releases
the modes: an explicit kill-pane on a pane with a -k mode leaves the other
panes alive. The upstream snapshot can instead kill their window by invoking
pane destruction again during mode cleanup. hmux preserves its single-owner
destruction contract rather than reproducing that teardown behavior.
A strong pane handle exposes &WindowPaneStorage, which implements WindowPane
and directly owns private window_pane state plus allocation metadata. Operations
use shared receivers with independent Cell and RefCell fields. Screen and
mode references are scoped to callbacks such as with_screen,
with_base_screen_mut, and with_active_mode; the borrow guards remain inside
the implementation. Small queries return copied values, and options access
retains a handle. Callbacks can access unrelated fields, while incompatible
borrows of the same field are checked at runtime. Screen mutation uses a
restricted view that preserves screen ownership.
Pane capabilities use static dispatch. The strong handle must remain alive
while its storage reference is used; RustWindowPaneWeak::upgrade returns
None after destruction. RustWindowPaneRef::detached creates an unregistered
owner. Resize queues and output offsets remain concrete component values owned
by the pane and control-reader state.
Control notifications are emitted after the enclosing command's %end or
%error, including pause/continue and configuration errors. Command output
that resembles protocol guards remains ordinary text. Pending command replies
are limited to 64 MiB; a client that reaches the limit exits with too far behind. Exiting clients stop receiving new pane output and notifications.
The server allows ten seconds to drain output and another ten seconds to
complete the exit handshake. wait-exit clients accept an empty LF-terminated
line or EOF after %exit. Respawning a pane drops its old queued control
output and resets pane, pipe and control-reader offsets together.
Windows use WindowRef(Rc<WindowStorage>) and matching weak observations.
Clones share immutable identity, independent Cell values and field-level
RefCell resources: names, saved layout, timers, pane membership, selection,
history, z-order and the layout tree. Options retain their checked option
handle, and ID registration belongs to storage rather than to the payload.
The public window value traits remain independently implementable; name and
saved-layout observations return owned strings so no reference escapes a
resource borrow. WindowRef::default provides an unregistered value facade.
Session winlinks strongly retain windows, so unlinking one of several links
does not destroy a shared window. Queued alerts and notifications may explicitly
retain a window until their deferred work finishes; ordinary timer callbacks
retain only weak observations. Window reverse registrations retain weak
session identities and indices, never owning sessions or duplicate winlinks.
WinlinkRef temporarily retains a session and follows the original link
allocation when a shuffle changes its index. Saved command and format
observations keep a weak session observation and the link identity, so removal,
renumbering, group synchronization, and index reuse make them expire. Explicit
numeric lookups create a fresh handle for the current occupant. Link snapshots
preserve insertion/index order as appropriate, skip removed entries, and defer
newly inserted entries.
A successful weak upgrade means an allocation still exists; registry membership
is a separate condition.
The layout engine remains in layout/. Its authorization permits separate
checked tree reads, tree-only edits and coordinated tree/pane edits. Coordinated
edits acquire the tree before pane membership, then update selection and
observational orders before publishing callbacks. Guards end before pane resize,
notifications, hooks and redraw. Same-window exchanges use a single resource
borrow. Rendering reads a scoped z-order slice; it does not clone pane membership
for each cell.
Pane operations may temporarily retain a window for deferred unzoom. Normal completion drains that edge, releases the operation's pane owner, and then unzooms/redraws. Unwinding releases the pane owner before the pending window without running redraw during panic, preventing an ownership cycle even when the callback releases the last external window owner. Direct pane destruction resets modes and releases resources before consuming its sole owner, which also releases pending deferred state. Final window destruction restores saved layout geometry, removes registration and layout state, destroys owned panes, disarms all three window timers and destroys its options.
Sessions use SessionRef(Rc<SessionStorage>) and SessionWeak(Weak<SessionStorage>)
with a private payload. The three entities now share the same allocation model:
clones share state and borrow checks; immutable identity belongs to the allocation;
copyable independent state uses Cell; mutable resources use separate RefCells.
Session name, directory, environment, group membership and lock timer have separate
borrow domains. Window map, current index and history share one small checked group.
Terminal settings and timestamps are copied values; options retain their existing
shared checked handle. Environment callbacks may access independent session state,
but overlapping access to the same environment is rejected at runtime.
The live-session registry owns sessions. Session groups, client attachments and command/format targets observe them weakly. Session winlinks own windows; windows own panes; panes observe their window weakly. Reverse window registrations use opaque RAII tokens containing weak session identities and link indices. No graph back-reference creates another owner. A retained session can outlive registry removal, and an old allocation cannot remove a replacement using its former name. Rename updates the registered key and owned name together.
Link consumers hold only WinlinkRef handles, which resolve through stable link
identity and read the current index. Explicit numeric target paths remain index
lookups. Checked link guards stay inside the session owner. Selection, replacement,
removal, renumbering and exchange release guards before notifications, redraw,
hooks or owner teardown.
Same-session exchange uses one map borrow. Group synchronization snapshots source
links, prepares recipient membership/selection/history, then publishes link
notifications in member/index order. Pending group transitions temporarily own old
links and windows until publication finishes; they are not another live registry.
Popup overlays own their screen, input context and job state. Border right-clicks
follow the popup mouse rules without installing a nested menu or menu-only
actions; popup input, dragging, resizing, closing and job completion remain on
the popup owner. display-menu continues to use the normal client/window menu
owner.
Notifications and explicit reactor-deferred session cleanup retain sessions until
dispatch. Job setup reads the session environment and adds no session owner. Lock
timers capture weak observations. Session destruction removes registry membership,
cancels its lock timer, leaves its group and releases links while other sessions'
shared windows remain alive. Final allocation cleanup releases owned environment,
options and other resources. SessionRef::default is an unregistered value facade;
its identity setter is for unique, unobserved construction ownership. Name and
directory trait observations return owned strings. No session payload, raw pointer,
or unchecked link-map reference is exposed to consumers.
Some registries and server state remain process globals. Global collection wrappers bind to their first accessing thread and reject other threads before touching their contents. A mode entry's parent-pane pointer is used only before mode teardown. Mode trees can outlive their modes; they resolve pane IDs and detach on mode closure. Some non-pane compatibility access still requires explicit unsafe accessors. Those callers must exclude conflicting borrows and reentrant teardown; reference counting alone does not enforce those rules. These are transitional boundaries, not a completed memory-safety audit.
Copy mode observes its source pane by ID. Its cloned screen survives source
pane destruction. r runs refresh-now; automatic refresh is controlled with
refresh-on, refresh-off, and refresh-toggle. Both refresh paths are available
only for a pane's own copy mode. Automatic refresh pauses while a selection or
cursor drag is active and follows new output only while the cursor remains at
the bottom. Scrolling a stopped selection preserves its text endpoints; dragging
inside it adjusts the nearest endpoint. Word navigation recognizes Unicode
whitespace and skips wide-character padding.
line-numbers-on, line-numbers-off, and line-numbers-toggle control the
copy-mode gutter, including when copy-mode-line-numbers is off. Emacs L
toggles the gutter. copy-mode-current-line-style styles the cursor's line;
copy_line_numbers and refresh_active expose the current mode settings.
copy-mode -k kills its pane when the mode exits.
Public Rust structs and functions are implementation surfaces. Existing public traits are versioned compatibility contracts; changes to their signatures or semantics require human signoff.
The 27 expansions of the BSD tree.h RB_GENERATE macros the transpile
carried — every *_RB_INSERT, *_RB_REMOVE, *_RB_FIND and their colour
helpers, about 12,600 lines — are gone: each tree is a BTreeMap keyed by
what its comparison read, and the rbe_* link fields are off the elements.
The keys keep the C order, so everything the order is observable through
(list-sessions, next-window, show-options, list-keys, list-buffers,
server-access -l, control-mode subscriptions) comes out as before. Session
window links also retain red-black tree shape metadata so
session_destroy sends window-unlinked notifications in tmux's root-removal
order after arbitrary insertions and removals. Ownership stays in the map.
One behavioral difference remains:
- A paste buffer whose order counter wrapped used to be dropped from the
by-time tree while the by-name tree kept it, since
RB_INSERTrefuses a key it already holds. The map replaces instead. Reaching this needs 2^32 buffers in one server.
The reference build it was transpiled from used the pinned oracle's configure
flags (--enable-systemd --enable-utempter --enable-utf8proc --disable-sixel,
plus its --sysconfdir/--localstatedir). Those features are compiled in, so a
flag that differed from the oracle would be a behavioural difference in every
result this crate produces: --enable-utf8proc alone decides whether
utf8_towc goes through utf8proc or libc mbrtowc, which changes both the
width tables and the errno left behind on invalid input.
Hook inspection accepts show-hooks -F with the option fields and
hook_fire_count/hook_fire_time. The count belongs to the option entry and
increments once per firing, including an empty hook, rather than once per
array command. Replacing commands preserves the count; removing the option
and recreating it resets the count. show-hooks lists user names registered
with set-hook; show-options includes those names when passed -H or when
requested explicitly. Registration of a user hook name lasts for the server
lifetime, even if its option is removed and later recreated.
Notification and after-command events dispatch to control, plugin and hook sinks in
registration order. Hooks select and retain their commands at event emission:
removing or replacing a hook later in the same command chain does not replace
those selected commands. Event hooks expose hook_event alongside hook.
Hook commands remain queued with nested hooks suppressed; their events still
reach control clients. Control notifications follow each command's reply guard,
so a compound line can interleave notifications between command replies.
Window-close notifications use the link state at event emission, before the
link is removed, and can therefore use %window-close where the 3.7b reference
used %unlinked-window-close.
Payloads retain session and window handles. Pane payloads preserve the pane ID
and retain the pane allocation through final payload release; a destroyed
pane's ID remains printable but its storage cannot be accessed through ordinary
observations. After-command hooks preserve argument and repeated-flag formats,
mouse context, and insertion before the next command. They expose hook_event,
select and count the hook at dispatch, and suppress nested after-command events.
Manual set-hook -R remains hook-only execution. Event targets follow a moved
pane when it remains live, then recover through the saved window link, session
and current default target when necessary.
Lifecycle events cover client and window creation/closure, pane creation and
respawn, pane movement, session groups, marking, zoom, mode transitions, output
activity, bells, and resizing. Payloads expose the affected objects and old/new
names, selections and dimensions. Mode entry/exit events precede the corresponding
mode-changed event. Title payloads capture each emitted title before queued hooks
run. Pane exit events wait for the child status, even when its terminal closes
first. hook_exit_signal follows the existing signal formatter and prints the
signal number (for example 15), matching the configured Linux target build.
Window closure fires once at the last strong window-handle release; an event sink
may retain that window without causing a second closure event when it releases
it. Removing a sink during dispatch defers releasing its captures until the
outermost dispatch finishes. Releasing a captured window can then emit its
closure event through the same dispatcher. Rust panic unwinding and teardown
after global options have been removed do not dispatch closure hooks. Pane
payloads continue to expose weak observations to ordinary consumers.
set-hook -E expands a user event name beginning with @ and dispatches it
with the selected client/session/window/pane context, including hook_event.
Emitting an event without a registered hook succeeds. set-hook -R remains
hook-only execution and does not notify other event sinks.
Shell/modal/prompt events remain follow-up work.
Raw string convenience methods have been removed. Arguments uses scoped
Option<&CStr> reads. SessionNameState and SessionDirectoryState return owned
CString snapshots, so callers can retain them after checked resource borrows end.
OptionsEngine::array_get and array_value now borrow values from their entry
or item. array_indices replaces the raw array_first/array_next cursor pair
with an ordered index snapshot, allowing each subsequent read or mutation to
have its own borrow. Hook consumers clone command handles through value_command
before queueing them. OptionsRef::string_ref replaces the raw string accessor
with an immutable Rc<CStr> snapshot retained independently of the store. Status,
copy mode, rendering, and command consumers borrow that snapshot while reading it.
OptionsRef::style_value replaces the cached style pointer
with an independent copy, which remains valid after an option changes.
OptionsRef::with_entry retains the owning store and borrows the entry only for
its callback. Scalar reads, option formatting, and option listing use this path.
local_names replaces the raw store cursor pair with a name snapshot. Parsing
and prefix matching return owned names before scoped lookup; scalar setters no
longer return entry pointers. OptionsEngine has no raw-pointer-returning methods.
with_entry_mut holds an exclusive scoped borrow for array edits; the native
adapter also tracks these borrows and rejects conflicting access through clones.
Hook queues retain cloned command handles, and status rendering retains array
strings through value_string_ref before releasing the entry borrow.
Removal and reset take a store and name after any entry borrow ends. Initializers
return no entry pointer; subsequent reads and edits use scoped callbacks.
The Rust store also initializes defaults without raw entry pointers and parses
styles from retained text. Format expansion runs outside the store borrow;
cache writes check that the option still holds that text after expansion.
LongOption descriptors retain a lifetime-bound flag borrow. Shared and mutable
flag accessors replace the raw flag pointer, and long-option parsing uses a
bounded descriptor slice. The native adapter retains the same borrow lifetime.
VariadicArguments borrows initialized backing regions as slices with shared
and exclusive accessors. VariadicCursor replaces the unused raw ABI cursor
record; the native adapter keeps the supplied region lengths and borrow lifetime.
SystemdJobWatch owns its optional path and returns a scoped string borrow. The
bus reply is copied when the watch is armed; the native adapter also retains an
owned path through replacement and clearing.
TerminalCommandData and TerminalCommandSelection borrow complete byte slices
and clipboard names. Drawing contexts carry those lifetimes through synchronous
output, including cloned contexts, without copying the payload. Native adapters
retain the same lifetime and expose scoped slice and string borrows.
UserAccount returns borrowed strings from owned account snapshots. Name and
UID lookups use the reentrant libc routines with caller-owned storage, and
server access, tilde expansion, user formats, and startup defaults retain those
snapshots while reading their fields. The native adapter owns its strings too.
CalendarTime retains its optional timezone in Rc<CStr> and returns a string
borrow. Clock drawing and time formats use owned local-time results; nested
format state clones retain the timezone handle. Raw calendar records exist only
for the duration of libc calls.
SystemdBusError owns snapshot strings and returns scoped borrows. Its recorded
ownership marker is metadata; native error cleanup belongs to the private guard
that captures the snapshot, independently of later changes to that metadata.
ImsgMessage borrows its original body range from the owned message buffer,
independently of the reader cursor. Shared and exclusive slice access replace
the stored raw data pointer. Client, server, and file dispatch decode bounded
bytes and strings; the native adapter owns the body behind its borrowed view.
Header encoding and decoding use fixed byte arrays instead of struct-pointer
casts, retaining imsg native byte order. Header readers receive the current
size limit as a value refreshed before each read, rather than retaining a raw
pointer to the transport owner. Initialized readers can move without leaving a
callback tied to their former address.
IoVector retains initialized storage through IoSliceMut and exposes shared
and exclusive slice access. Queued writes use IoSlice borrows and reads use
IoSliceMut borrows through the syscall. Descriptor bookkeeping uses the queue
owner after those borrows end; native adapters retain the same slice lifetime.
The buffer adapter borrows initialized bytes and spare capacity through
BytesMut's separate slice interfaces.
ControlMessageHeader borrows a complete initialized ancillary buffer. Its
payload accessors return bounded shared or exclusive slices, and length updates
must fit that buffer. Received ancillary messages are decoded through disjoint,
validated byte ranges instead of header-pointer arithmetic. Native comparisons
cover the platform header encoding, including storage without header alignment.
MessageHeader retains exclusive borrows of its optional address, I/O vector
array, and initialized ancillary storage. Its accessors return scoped slices and
its setters retain the replacement buffers. Socket receive calls keep those
borrows through recvmsg; the send path borrows immutable regions through
sendmsg. Native adapters retain the same buffer lifetimes and enforce storage
bounds on length changes. Raw libc socket headers are local to syscall adapters;
the exported header copies and the ancillary-storage union are removed.
CompiledRegex compiles owned libc patterns and returns match offsets for bounded
borrowed strings. The automaton and lookup-table pointers are no longer exposed:
callers use matching operations instead of inspecting opaque libc storage.
Substitutions, format matching, pane search, and copy-mode search share the same
owned guard, so cleanup follows scope and early returns. The chosen POSIX engine
and its per-context matching flags are preserved.
Arithmetic format expressions keep Rust's defined numeric conversions. tmux
casts floating-point results to long long; that conversion is undefined for
NaN and out-of-range values and produces platform-dependent results. hmux does
not reproduce those undefined results.
Terminal feature lookup rejects names absent from the feature table. tmux 3.7b
leaves its cursor on the final table entry after a miss, so an unknown name can
report the final feature (usstyle) as present when that entry's capabilities
exist. hmux keeps lookup results independent of table order.
GlobResult owns its path slots and safely borrows individual names. Expansion
uses a private libc result, copies names into shared Rc<CStr> storage, and
releases the native allocation before returning. source-file moves those names
into its work queue and clones their handles for asynchronous reads. The raw
path-vector constructor and public native glob record are removed; libc remains
the pathname-expansion engine.
This migration removes forty-four raw-returning trait signatures. The audited traits no longer return raw pointers. Unsafe mutation and native ABI operations still carry caller obligations; this does not make those operations safe. The tmux command and wire behavior is unchanged by these migrations.
src/main.rsforwards command-line arguments to the library entry point insrc/tmux.rs.src/cmd/andsrc/server/implement command dispatch and server lifecycle.src/screen/,src/grid/,src/input/, andsrc/tty/handle terminal state, input parsing, and attached-client drawing;src/control/handles control mode.src/options/owns option storage and inheritance.src/types.rsretains shared entity types and transitional compatibility exports.src/reactor/adapts thehmux-rt/runtime to daemon callbacks and I/O.src/fmt_engine.rsimplements C-style formatting usingFmtArgslices in place of C varargs. Format strings and output storage use slice access, and borrowed C-string and byte-slice arguments retain their lifetimes through formatting. Byte strings stop at their slice boundary, NUL, or precision limit. Raw-pointer string arguments remain for callers awaiting migration.src/ffi.rsdeclares external C functions.src/tests/and subsystem-local test modules cover engine behavior. The parent repository supplies the tmux conformance harness and validation gates.hmux-agent/contains the shared agent-classification implementation.
nix develop
make # cargo build
make check-tmux # refuse a reference tmux that is not 3.7bmake clean runs cargo clean.
The server option clear-on-attach defaults to on. Set it to off to scroll
the previous terminal contents into scrollback without entering the alternate
screen. Detaching clears the current screen; terminal modes are restored in
both settings.
In terminal-features, append @ to a feature name to disable its automatic
addition, including additions from terminal replies. This does not remove
capabilities already supplied by terminfo; use terminal-overrides for those.
The utf8 feature marks the client as UTF-8, and #{I/f:utf8} reports the
client's UTF-8 flag, including locale and -u detection. A feature disabled in
a client -T list is removed from that list; persistent discovery overrides
belong in terminal-features.
Synchronized output (DEC mode 2026) records changed base-screen rows. Resetting that mode, or its one-second timeout, flushes those rows through the existing screen writer. A full pane redraw clears the pending rows; resizing and pane teardown release them. Control-client output keeps its separate path.
Dirty-row selection follows the pinned tmux, including deletion outside the scrolling region during a synchronized update. With a scrolling region covering rows 2–4, deleting row 12 clears it in both grids, but marks rows 2–4 dirty. The old row 12 remains visible until a later redraw, matching the oracle.
Attached clients cache window geometry as ordered spans with weak pane, window
and menu observations. Layout, viewport and menu changes rebuild geometry;
content and style updates use the existing scene. Pane, scrollbar, border/status
and window-menu repaint requests remain distinct. Client overlays retain their
independent clipping path. The renderer uses the pinned tmux next-3.9 scene
algorithm with hmux's Rust ownership, grid, formatter and terminal encoder.
The configured pin disables SIXEL; SIXEL-only drawing is not covered.
Pane prompts draw their label and input without filling the unused part of the
row. Status prompts clear their message area. Prompt format expansion uses
prompt_input, and quoted input is drawn relative to the input's start column,
matching the pinned tmux.
The public Rust ScreenRedrawContext trait and screen_redraw_ctx data type
remain deprecated compatibility facades. Runtime rendering and overlay callbacks
no longer use them.
Formatted status and mode text accepts #[link=https://example.com]text#[nolink].
Links continue across other style changes until nolink, default, or an empty
link= resets them. They are emitted only when the terminal supports hyperlinks.
URIs cannot contain style delimiters (spaces, commas, or newlines); each style
word must fit within 255 bytes. OSC 8 input rejects URIs longer than 1,024 bytes
after escaping. Styled links reuse the existing hyperlink stores and eviction
policy, with stable screen IDs across redraws.
capture-pane -I prefixes captured lines with their history timestamp in Unix
seconds (zero when unavailable). -R dumps the base grid, including line
allocation counts, decoded cell flags, attributes, colours and hyperlinks;
it takes precedence over the other capture selection flags. History additions,
collection and reset/reflow generations are available as history_added,
history_collected and history_generation. pane_output_generation advances
for each nonempty input batch, so its exact count depends on input batching.
Grid timestamps retain the existing absolute-time representation.
history_bytes/history_all_bytes report the byte costs of the equivalent tmux
grid, including its compact line metadata layout. These compatibility formats
do not measure Rust allocation overhead.
OSC 133 prompt, command, output-start and output-end markers are stored on grid
lines with their cursor columns and command exit status. capture-pane -R
includes this metadata. A and N emit pane-shell-prompt, C emits
pane-command-started, and D emits pane-command-finished; command events
carry status, start/end times and duration when those values are available.
The latest observations are exposed through pane_last_output_time,
pane_last_prompt_time, pane_command_start_time, pane_command_end_time,
pane_command_running, pane_command_duration and pane_command_status.
Invalid numeric statuses become 255, while a missing status or a leading
key/value parameter becomes zero, matching the target parser.
Ordinary clients receive version 2 JSON from window_layout and
window_visible_layout. Control clients retain the checksummed legacy format
until they enable new-layouts; refresh-client -f !new-layouts disables it
again. Layout notifications use each recipient's negotiated format.
select-layout accepts both formats. JSON preserves pane indexes, active and
last-pane state, and floating z-order. Legacy output omits floating panes, and
applying a legacy layout preserves existing floating panes. Both codecs use
the existing owned Rust layout tree; legacy output projects that tree without
mutating it.
The owned layout engine supports floating-pane creation and splitting, tiled and floating resizing, and float/tile transitions. Tiled resizing and removal skip floating-only branches, redistribute space to eligible neighbors, and stop when no further resize is possible. Pane geometry accounts for status rows and reserved scrollbar padding at minimum sizes.
Floating insertion follows the target's position in the tree. Saved floating geometry survives retiling, preset rebuilds and zoom restoration without adding private state to layout JSON. Floating splits support both axes, before/after placement, pane borders and full-window sizing. Differential regressions check these operations against the pinned tmux target, including nested operation sequences and custom-layout roundtrips.
display-message -j validates and prints the same restricted JSON subset:
objects at the root, arrays of objects, signed 64-bit integers, booleans and
nonempty strings. Null and fractional numbers are rejected. Escapes are
validated and preserved, keys are sorted, and duplicate keys are rejected.
JSON object nesting is limited to 200; legacy layout nesting is limited to 1000.
Hyperlink sets share an eviction registry within their server thread. Each set retains the registry owner through cleanup, including thread exit; separate threads have independent registries and ID counters. Exhausted hyperlink ID counters fail explicitly rather than recycling identities that grid cells or queued eviction entries may still reference.
Key tables, named wait channels, and paste buffers also use checked thread-local registries. Queue wakeups, client updates after table removal, and paste-buffer notifications run after their registry borrows end, so observers can revisit the committed state. The starting environment uses checked thread-local storage; command output and expansion callbacks consume owned snapshots after its borrows end. Other server collections are still being migrated.
Named channels support wait-for -l name and wait-for -w client name.
Listing reports live ordinary waiters before lock waiters, in registration order
within each group. Explicit wake releases the first matching waiter without
signalling the channel or transferring a held lock; missing waiters succeed
silently. Released queue items remain weak observations and are omitted from
listing and wake selection.
Event waits use wait-for -E name, with unprefixed payload formats in -F
and public key=value payload output in -v. Verbose output includes events
rejected by the filter; internal keys beginning with _ are omitted. Events
are not sticky: only waiters already registered when an event fires can wake.
-E -l lists waiters in registration order, and -E -w client releases the
first matching waiter, returning an error when none exists. Listing takes
precedence over waking. Built-in hook names and names beginning with @ are
accepted without requiring a registered hook. These waits share the event
dispatcher used by hooks, while set-hook -R remains hook-only.
Event registrations observe queue items weakly. Releasing the last queue-item owner cancels the registration and removes its sink; flush resumes all live waiters and removes every registration. Retaining a queue item can therefore extend its registration beyond queue removal. This follows the existing Rust queue ownership boundary rather than upstream's raw item lifetime.
UTF-8 width overrides and temporary width suppression are confined to the calling thread. Packed character IDs use a shared, synchronized intern store so encoded grid characters retain their meaning across callers and threads.
Pane, window, and session IDs are never recycled within a server thread. After issuing
the final 32-bit ID, subsequent creation panics instead of wrapping to zero and
allowing a stale observation to address a replacement entity. This differs from
the reference at ID exhaustion; pane activity-order stamps retain their existing
wrapping behavior. The next_session_id format is empty once session IDs are
exhausted.
Job IDs also never wrap. Once exhausted, starting a job returns failure with
EOVERFLOW before opening descriptors or forking; callback data is released.
Failed startup attempts consume their reserved ID.
Collected screen items use a shared, synchronized pool with FIFO reuse. Released items remain readable until reuse; allocation panics before an index could become the reserved sentinel or wrap to an existing item.
Hook monitors (set-hook -B, optionally -T for changed values that are true)
and control subscriptions (refresh-client -B) share one polling engine. The
engine checks formats once a second without shell jobs, tracks each pane or
window at each link index independently, and sweeps removed links from all-target
subscriptions. Control clients receive initial values; hook monitors establish
an initial value silently and report subsequent changes with hook_value and
hook_last. Monitor hook commands expand their formats before parsing and run
only the command in the monitor's exact option store. show-hooks -B/-BF exposes
the selector, format, firing count and firing time. Removing a monitor with
set-hook -uB preserves its command; removing the option destroys the monitor.
Monitor timers observe their option owner weakly; client subscription timers observe their client weakly. Local hook monitors resolve a non-recycled session ID through the live registry, while global monitors use the first live session. They do not retain a closed session allocation as upstream's monitor does. Polling snapshots retain subscription values through a scan and release their borrows before reporting events. Each monitor owns a dispatcher sink in registration order; replacement removes the old sink, and ordinary user-event emission does not run a monitor-only hook. Dropping the option releases its timer and sink. These ownership and scheduling boundaries remain those of the Rust components, rather than a claim of identical libevent object lifetimes.
The daemon uses the repository's hmux-rt runtime and its mio readiness
backend. Timers, descriptor watches, signals, deferred callbacks, and buffered
streams are represented as runtime tasks; the stream input and output sides use
the same segmented Buf implementation.
The compatibility host dispatches at most 64 ready tasks before and after a poll, and a stream drains at most 64 read or write operations before yielding. When idle, the host waits for at most 10 ms before returning to the daemon's housekeeping loop. Consequently, simultaneous timer, I/O, signal, and deferred work may be delivered in a different order or batch size than the reference libevent loop. This is an intentional scheduling boundary; the existing callback interfaces and wire-facing behavior remain the compatibility target.
The server carries a plugin layer: a plugin is a bundle of format variables
tmux does not have, plus whatever work it takes to keep them current. It
publishes an id-keyed dictionary — pane id and variable name in, string out —
which format_find consults after its own static table, so a plugin's
variables expand anywhere a built-in one does: status formats, list-panes -F,
display-message, and control-mode refresh-client -B subscriptions.
Writing one is implementing plugin::Plugin:
fn name(&self) -> &'static str; // enabled by this name
fn variables(&self) -> &'static [&'static str];
fn interval(&self) -> Option<Duration>; // how often tick runs
fn option_defaults(&self) -> &'static [(&'static str, &'static str)];
fn start(&mut self, host: &dyn Host);
fn tick(&mut self, host: &dyn Host);
fn resolve(&self, pane: PaneId, key: &str) -> Option<String>;
fn on_notify(&mut self, event: &Event<'_>);
and handing it to plugin::register. Nothing else in the server has to learn
about it: the variables start expanding, a shared timer picks up the tick, and
the option defaults go in. A built-in plugin is one line in plugin::builtins.
Values are pulled, not pushed. resolve runs only when an expansion actually
names one of the plugin's variables, so an expensive value costs nothing in a
format that never mentions it — which is why the trait has no lazy-value arm.
What a plugin can read is plugin::Host: the pane observability contract from
the hmux-agent crate — pane ids, child process, output revision, screen tail,
title — plus invalidate(pane), which marks the pane's window for a status
redraw. Panes are named by id and resolved per call, so plugin state can never
reach a destroyed pane through a pointer it kept.
TMUX_C2RS_PLUGINS is a comma-separated list of plugin names, or all, or
none. Unset runs the default set, which is the agent and git plugins: a
server nobody has configured is the one worth running.
TMUX_C2RS_PLUGINS=none — or an empty value, which is what a shell leaves
behind for a variable someone wanted cleared — turns every plugin off, and a
server running none is byte-identical to tmux: the two format hooks read one
thread-local flag and return, and no option default is touched.
That is the setting the conformance suite runs the subject under, and
scripts/hmux-sut.sh sets it there for the same reason it already passes
-f /dev/null: the comparison is of the engine, and the plugin's status line
is not something the oracle draws, so it would land in every rendered
comparison as a difference that is not a finding. The identity stays reachable
and stays measured; it is just no longer what an unconfigured server does.
The agent plugin — on unless TMUX_C2RS_PLUGINS says otherwise — adds the six
pane variables of ../PROTOCOL.md §2 —
#{pane_agent}, #{pane_agent_state}, #{pane_agent_pid},
#{pane_agent_session_id}, #{pane_agent_model}, #{pane_state_emoji} — by
polling every pane at 200 ms. The detection is not in this crate: detectors,
session-id and model resolution, process probing and the pane classifier live
in hmux-agent, which the hmux daemon hosts through the same contract, so
both servers classify a pane with one implementation rather than two that
drift. What is here is the wiring: the ServerObservability implementation over
window_pane, the tick, and the redraw.
These differences from the oracle are deliberate and expected, and are what
TMUX_C2RS_PLUGINS=none takes back:
- The six variables exist. Stock tmux expands an unknown
#{...}to nothing, so five of them read the same either way, but#{pane_agent_state}saysnonewhere tmux says nothing at all, and#{pane_state_emoji}is never empty. window-status-formatandwindow-status-current-formatdiffer, because the status line this server draws is built around#{pane_state_emoji}. That default is the server's rather than this plugin's — see below.- Each pane's output bumps a revision counter, and each pane is probed through
/proc(orlibproc) once per sweep. Nothing observable follows from either, but the server is doing work tmux is not.
exit-empty is not changed. The hmux0 daemon defaults it to after-session
and creates session 0 on a first untargeted attach; that is a lifetime change
rather than a presentation one, and this server keeps tmux's behaviour.
The git plugin — also on unless TMUX_C2RS_PLUGINS says otherwise — answers
where a pane sits in a git worktree, and what the repository holding it is in
the middle of. It exists because #{b:pane_current_path} is the wrong label
in a repository with worktrees: every worktree of this one has an hmux
directory, so the window labels collide and the component that tells them
apart is the one the basename drops.
| Variable | Values | Meaning |
|---|---|---|
#{git_worktree} |
h1, or empty outside a repository |
The worktree root's own directory name. A linked worktree is named by itself, not by the repository. |
#{git_worktree_path} |
absolute path | The worktree root. |
#{git_subdir} |
hmux/src, empty at the root |
Where the pane sits below the root. |
#{git_repo} |
hmux |
The repository every worktree of it shares, from the directory holding the common git directory. |
#{git_branch} |
h1, empty on a detached HEAD |
The branch HEAD names; during a rebase, the branch being rebuilt. |
#{git_head} |
h1 or 38b63b0 |
The branch when there is one, the short commit when there is not. Never empty in a repository. |
#{git_action} |
empty, rebase, am, merge, bisect, cherry-pick, revert |
The operation the repository is in the middle of. |
#{git_action_step} / #{git_action_total} |
2 / 7, or empty |
How far a rebase has got, when it counts. |
Every value is read out of files — the upward walk for .git, the HEAD it
names, and the marker files an interrupted operation leaves behind. Nothing
here runs git or reads the index, so there is no dirty-state tier: git status in a status line is the reason gitstatusd exists, and none of the
variables above need it. A sweep costs one readlink per pane and two stats
per repository, at 500 ms, and the repositories are shared — a dozen panes in
one worktree are one entry. Values are computed on the tick, so expanding a
status format never touches the filesystem; a pane created between two ticks
reads as empty until the next one.
Three things it deliberately does not do:
- The two rebase backends are one
rebase. The marker that looks like it separates an interactive rebase from a plain one is written for every rebase the merge backend runs, so reporting it would be wrong for the common case rather than right for the rare one. - A repository whose refs live in a reftable reports no branch and no commit.
There is no ref file to read there, and the placeholder git leaves in
HEADfor older readers is not a branch name. Everything else — the worktree, the repository, the operation — still answers. - The pane's working directory comes from the server's own pane tree rather
than through
plugin::Host, which carries no working directory. A plugin wanting to run on the hmux daemon as well would need one; adding it is a change to a versioned public trait, so it waits for a reason.
The window label these variables are for is window-status-format, and it is
the server's, not a plugin's: the plugins publish variables, and what the
status line does with them is decided in one place — server::defaults —
rather than in whichever plugin happens to name them. Two plugins declaring one
format would also make registration order decide it, since an option default
only replaces a value still holding tmux's.
It is the pane's state glyph, then where the pane is: the worktree name at the
root, the worktree name and a trailing / anywhere below it, the directory's
own basename outside a repository, and the operation in brackets when there is
one.
h1 h1/ proj h1 [rebase 2/7]
Nothing is replaced when no plugin is running. Every variable the format draws
on comes from one, and a server with none of them is meant to be tmux. With
some of them running, a variable whose own plugin is off expands to nothing,
and every branch of the format is written to survive that. Only options still
holding their built-in default are replaced, and this runs before any
configuration file is read, so .tmux.conf still wins.
The hmux executable reports tmux 3.7b and speaks the pinned client's wire
protocol. Unit tests exercise Rust components, and the parent repository's
conformance harness compares hmux against the reference tmux found on PATH.
The reference must report exactly tmux 3.7b.
From the parent repository, make unit, make test, make lint, make asan, and make leak
default to the active hmux implementation. make test runs unit tests and the
main conformance suite. Per-command, hook, queue, notification, and VT corpus
suites have separate Makefile targets. The hmux conformance profile also runs
ignored cases, with a small explicit exclusion backlog in the parent nextest
configuration. Passing SUT=hmux0 selects the retired daemon's gates.
The main conformance suite also runs the release hmux client through basic
start, attach, shell-exit, and detach lifecycles against the tmux oracle.
make test-client runs these cases alone. The gate builds the optimized client
and passes its path as HMUX_CLIENT_BIN; server conformance continues to use
the selected server build.
The unit gate runs each test in its own process with nextest, then runs doctests
through cargo test --doc. Global collection thread bindings last for the
process lifetime, so tests that use process globals require these isolated
gates. The leak gate also uses separate test processes; neither leak detection
nor conformance establishes aliasing soundness. The ASan gate checks address
safety with leak detection left to the separate leak gate. Resolver linkage
explicitly retains libresolv because ASan supplies base64 interceptor symbols
that otherwise let the linker discard the real implementation. Socket receives
use full address storage before copying a bounded prefix to the caller, so
ASan's receive interceptor can check the returned address length.
From this directory, make check-buffer-memory runs the segmented-buffer tests
under AddressSanitizer on nightly Rust for x86_64-unknown-linux-gnu. The suite
includes deterministic operation sequences compared with a contiguous byte
model, and checks oversized trait copies without changing the inherent method's
clamping behavior. This focused gate does not cover other daemon components or
replace the full conformance and leak gates. Miri is not required by this target.