Skip to content

Latest commit

 

History

789 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

hmux

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/:...}).

What the source is

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_INSERT refuses 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.

Trait pointer migration

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.

Layout

  • src/main.rs forwards command-line arguments to the library entry point in src/tmux.rs.
  • src/cmd/ and src/server/ implement command dispatch and server lifecycle.
  • src/screen/, src/grid/, src/input/, and src/tty/ handle terminal state, input parsing, and attached-client drawing; src/control/ handles control mode.
  • src/options/ owns option storage and inheritance. src/types.rs retains shared entity types and transitional compatibility exports.
  • src/reactor/ adapts the hmux-rt/ runtime to daemon callbacks and I/O.
  • src/fmt_engine.rs implements C-style formatting using FmtArg slices 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.rs declares 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.

Run

nix develop
make            # cargo build
make check-tmux # refuse a reference tmux that is not 3.7b

make clean runs cargo clean.

Terminal attachment

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

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.

Scene rendering

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.

Styled hyperlinks

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.

Shell integration

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.

Layout serialization

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.

Event loop

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.

Plugins

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.

Enabling them

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

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} says none where tmux says nothing at all, and #{pane_state_emoji} is never empty.
  • window-status-format and window-status-current-format differ, 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 (or libproc) 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

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 HEAD for 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 default status line

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.

Testing

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.

About

tmux-compatible server built for the people who work with coding agents

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages