term-wm is a high-performance terminal window manager and multiplexer featuring asynchronous PTY handling, tree-based tiling, and detachable sessions.
Designed for Linux, macOS, and Windows, term-wm brings the spatial organization of a traditional graphical desktop environment (like GNOME or KDE) directly to the command line. Whether you require mathematically precise tiling for development workflows or overlapping floating windows with mouse support, term-wm delivers a native window management experience without requiring a display server.
See the changelog for history (starting with v0.9.0-alpha).
Build and run from source (Rust 1.85+, edition 2024; no extra toolchain needed):
git clone https://github.com/jzombie/term-wm
cd term-wm
cargo run --releaseThis opens a new default workspace with two terminal windows by default. On first launch a detached background session daemon is auto-spawned, so workspaces and their sessions persist across terminal restarts and SSH disconnects — inspect or stop it with --list-channels / --stop-daemon, and pick a different workspace with -w. Pass programs as arguments to open them in new windows:
cargo run --release -- vim
cargo run --release -- -n 4 # open 4 windows
cargo run --release -- -n 3 -- ls -la # 3 windows; the first runs `ls -la`
cargo run --release -- -r "vim -l" -r "htop" # 2 windows, one command each
cargo run --release -- -n 4 -r "vim -l" -r "htop" -- git log --oneline # 4 windows: 3 commands + 1 default shellOptions (term-wm -h):
-n, --count <N>— number of windows to open (default 2; min 1); only takes effect on new sessions--scrollback <N>— scrollback buffer size per terminal window (default 2000); only takes effect on new sessions-r, --run <CMD>— command to run in a window; repeatable, one window per--run. A trailing-- CMD...runs one command in a window after the--runwindows. Remaining windows launch default shells. Only takes effect on new sessions.-w, --workspace <NAME>— workspace to open (defaultdefault); each workspace maps to its own daemon channel<workspace>/mainwith its own PTY session and window-manager instance--no-wm— run without the window manager (headless session client mode)--stop-daemon— stop the running background session daemon--list-channels— list channels and their sessions/clients, then exit-f, --force— force--stop-daemoneven when sessions/participants are active--no-session-persistence— disable session-persistence behavior at runtime (workspaces, gateway, daemon modes); only effective when thesession-persistencefeature is compiled in (it is by default)-h, --help,-V, --version
New terminal windows launch the shell from $SHELL (Unix) or %COMSPEC% (Windows).
| Action | Key |
|---|---|
| Open Command Palette (Super Key) | Ctrl+A |
Send Ctrl+A to the focused app |
Ctrl+A (When Command Palette is open) |
| Cycle focus between windows | Tab / Shift+Tab (When Command Palette is open) |
term-wm automatically enters Direct Input Mode (unfiltered, zero-delay key/mouse passthrough) whenever a child app requests the alternate screen buffer, mouse tracking, or custom scroll margins.
Direct Input Mode is split into two independent dimensions: keyboard (alternate screen / custom margins → raw key passthrough) and mouse capture (the app explicitly requested mouse tracking). Keyboard and mouse are granted independently — an app on the alternate screen without mouse tracking (e.g. pico/nano) keeps native text selection and wheel scrolling.
This mode is application-specific and different windows running different applications can be in different modes at once.
In Direct Input Mode, the following keybindings are not-effective, and are contingent upon the app running inside the window to handle them.
| Action | Keybinding / Input |
|---|---|
| Scrollback Navigation | PageUp / PageDown / Home / End |
| Scroll One Line | Shift + Up / Shift + Down |
| Select & Copy Text | Mouse Click & Drag (release to copy) |
| Paste | Mouse Right-Click |
Note on Clipboard Sync: Clipboard behavior depends on your host OS and terminal emulator. Standard keyboard shortcuts (e.g.,
Cmd+C/Cmd+Von macOS,Ctrl+Shift+C/Ctrl+Shift+Von Linux/Windows) may work depending on your terminal's pass-through rules, but are not guaranteed.
Clipboard split-brain:
term-wmkeeps an internal clipboard alongside your OS clipboard. In most setups they stay in sync, but where the OS clipboard is unreachable — e.g. inside a terminal that doesn't support OSC 52, or over SSH — the two can diverge. Paste is one unified action: it reads the OS clipboard when available and otherwise falls back to the internal copy, so you never have to pick between them. It is bound to mouse right-click, and if a Direct Input Mode app is consuming right-click, Paste is also available from the Command Palette.
Clipboard enablement in Direct Input Mode: While a window is in Direct Input Mode,
term-wm's mouse-managed clipboard integration — click-and-drag selection copy and right-click paste — is overridden: mouse events are forwarded to the running application unfiltered, and clipboard handling within that application is the application's responsibility. Application-initiated copy continues to work, as OSC 52 copy sequences emitted by the running application are still intercepted and relayed to the system clipboard.
term-wm is designed to be highly resilient, running anywhere a standard terminal environment is available, but relies on modern terminal standards for its optimal presentation.
- Colors: Truecolor (24-bit) support is highly recommended. The application will gracefully degrade its color palette in 256-color or 16-color environments, but UI themes and drop shadows are designed against 24-bit depth.
- Unicode & Fonts: Requires a UTF-8 compatible environment and a font capable of rendering standard Unicode box-drawing characters to properly construct window borders and layout splits.
- Linux Virtual Terminals (TTY):
term-wmis fully usable in raw Linux VTs (e.g., accessed viaCtrl+Alt+F1). While the core window management and multiplexing logic remains 100% functional, visual presentation will look significantly different due to the kernel framebuffer's strict font and color limitations. - Non-Standard OS Installs: Minimal or headless OS installations must ensure a valid
terminfodatabase is present and that theLANGenvironment variable is correctly set to a UTF-8 locale to prevent layout corruption.
See docs/COMPATIBILITY.md for full compatibility details.
term-wm is engineered with a strict modular architecture, separating core domain logic from presentation across a multi-crate Cargo workspace, with the draw pipeline built on Ratatui. Layout calculation, rendering, and PTY I/O are decoupled so the UI thread never blocks on I/O.
| Crate | Primary Responsibility |
|---|---|
term-wm-core |
State engine, generational WindowKey slotmaps, command palette, Reaper thread |
term-wm-layout-engine |
Generic tree layout algorithm (BSP + N-ary nodes), aspect-ratio rebalancing |
term-wm-pty-engine |
Dedicated PTY reader threads, drain-sync resize, PtyStateTracker direct-input detection |
term-wm-console |
Crossterm backend, DrawPlanRenderer, screen-space HitboxRegistry |
term-wm-render / term-wm-events / term-wm-crossterm-adapter |
Render backend trait, event types, input translation |
term-wm-ui-components / term-wm-sys-ui-components |
Component library + WM system chrome (panels, palette, help) |
term-wm-config |
Std-only leaf crate: session-persistence feature gate, process-global runtime config, canonical TERM_WM_* env-var constants |
term-clipboard / term-sys-io |
Cross-platform clipboard (arboard + OSC 52 backends) and low-level OS FD/handle redirection |
term-session* (+ term-size-box, term-bench) |
Detachable client/server session protocol (muxio), workspaces, sizing, benchmarks |
Windows are identified by generational slotmap keys (WindowKey): closed keys are never reused. The open path is a single transaction — register the component (spawn, which fires its on_mount hook), map it, tile or float it, then focus it.
The layout engine builds a tree (BSP or N-ary) over the workspace. insert_window_balanced fills empty void nodes first, then splits the largest leaf; the split axis is chosen by whichever dimension fits, falling back to aspect ratio, and leaf areas are rebalanced to equal shares by leaf count.
The UI event loop runs synchronously on a single thread and never blocks on I/O. All asynchronous work (PTY reading, network IPC, keyboard input) runs on separate threads or Tokio tasks and funnels events into a single crossbeam-channel–backed UnifiedEventSource.
[ Muxio / Network IPC ] ──(Tokio Runtime)──┐
├──> [ UnifiedEventSource ] ──> [ Centralized UI Loop ]
[ PTYs & Keyboard Input ] ─(OS Threads)────┘ (crossbeam-channel) (Single-threaded &mut)
A dedicated Reaper thread reaps zombie children via SIGHUP→SIGKILL escalation. The centralized loop drains all pending events per frame, so keyboard shortcuts, PTY output, and remote IPC are all processed with zero polling gaps.
PtyStateTracker (term-wm-pty-engine) monitors the PTY byte stream for alternate-screen or mouse-tracking requests. When active, term-wm enters Direct Input Mode, bypassing window manager keybinding/focus evaluation and eliminating ESC sequence buffering to hand raw input to applications like Vim or Less via zero-delay pass-through.
CoreEngine builds a z-ordered DrawPlan each frame; DrawPlanRenderer paints it, while a screen-space HitboxRegistry routes mouse hits to the correct component. A frame pacer targets a smooth 60 FPS, and a power profile tracker scales the frame rate down during idle periods to preserve battery life.
The component system renders to in-memory buffers (Buffer + UiFrame) with test doubles (TestPane, TestComponent), so layout, rendering, and PTY scroll synchronization are verified without a terminal — including property tests for scroll sync.
Line coverage is tracked via Coveralls using cargo-llvm-cov (see the CI coverage job in .github/workflows/rust-tests.yml). A root Makefile makes the same measurement reproducible locally:
make coverage # clean + full coverage run (workspace, all features) + summary
make coverage-baseline # as above, and tees the summary to coverage-baseline.txt
make coverage-main # coverage of the `main` branch via a throwaway git worktree (.build/main-worktree)
make coverage-clean # remove the worktree and coverage artifactsPrerequisites (once): rustup component add llvm-tools-preview and cargo install cargo-llvm-cov (or cargo binstall cargo-llvm-cov). Coverage output is written to lcov.info (git-ignored). The workflow mirrors the CI commands exactly, so a local run reproduces the Coveralls numbers up to platform differences.
- Hybrid Layout Engine: Seamlessly mix Binary Space Partitioning (BSP) and N-ary tree tiling with a free-floating window layer. Floating windows support mouse-driven repositioning, edge-snapping, and Z-index drop shadows.
- Adaptive Viewports: Quickly switch to Maximized mode to fill the workspace with the focused pane, or engage Monocle mode to view a single window full-screen—ideal for narrow viewports or mobile SSH sessions.
- Self-contained Session Persistence & Workspaces:
term-wmembeds both the window manager and a background session daemon (gateway). On first launch a detached gateway is auto-spawned and the UI runs as an inner session-backed process — so windows, layout, and running PTY processes survive terminal-emulator restarts and SSH disconnects. Named workspaces (e.g.default,dev) each map to their own daemon channel (<workspace>/main) with an independent PTY session and WM instance; create and switch between them from the Command Palette (New Workspace, Switch to Workspace:<name>), or detach the current viewer without killing its process (Detach Viewer). For a standalone, layout-agnostic persistence layer (no window manager), the companionterm-sessiondaemon remains available. See Workspaces & Session Persistence.
term-wm is a self-contained binary that embeds both the window manager and a background session daemon (gateway). On first launch a detached gateway is auto-spawned and the TUI runs as an inner session-backed process, giving you persistent sessions without any external daemon setup.
- Workspaces: A workspace is a named channel namespace on top of the session daemon. Each workspace (e.g.
default,dev) maps to a daemon channel<workspace>/mainwith its own PTY session and window-manager instance. Start in a workspace with-w/--workspace <NAME>. - Switching workspaces: From the Command Palette, use New Workspace to create one, Switch to Workspace:
<name>to switch without restarting the process (the viewer's IPC is rebound to the target channel, and the previously shown workspace keeps running in the background), and Detach Viewer to disconnect the current viewer from its session without terminating the PTY process. Workspace entries appear only when session persistence is active. - Environment-scoped gateway: The gateway endpoint is
term-wm/<env>/<user>/gateway.<env>defaults todevin debug builds andprodin release, and can be overridden withTERM_WM_ENV=dev|prod|test— so a development build can never attach to or tear down a production daemon's sessions.TERM_WM_GATEWAYoverrides the endpoint wholesale. Bothterm-wm --helpandterm-session --helpprint aPersistence gateway:footer showing the resolved endpoint. - Runtime disable: Pass
--no-session-persistence(or setTERM_WM_NO_SESSION_PERSISTENCE) to disable workspace/session-persistence behavior at runtime, even when the feature is compiled in. - Managing the daemon:
--list-channelsshows every workspace channel, its session, and its attached clients;--stop-daemonshuts the background gateway down (refused while sessions are live unless-f/--forceis given);--no-wmruns a headless session client without the window manager.
| Variable | Purpose | Default |
|---|---|---|
TERM_WM_ENV |
Runtime environment (dev/prod/test, case-insensitive); scopes the gateway endpoint. |
dev in debug builds, prod in release |
TERM_WM_GATEWAY |
Wholesale override of the gateway endpoint. | term-wm/<env>/<user>/gateway |
TERM_SESSION_CHANNEL |
Session channel override (read by term-session). |
default/main |
TERM_WM_NO_SESSION_PERSISTENCE |
Disables session-persistence behavior at runtime (same as --no-session-persistence). |
unset (persistence enabled) |
TERM_WM_TRACE_ESC |
Dumps raw PTY→emulator bytes to a file (debugging aid). | off |
Traditional terminal multiplexers often collide with the keybindings of the applications running inside them. term-wm is deliberately minimally invasive: its keybindings primarily listen for the Ctrl+A Super Key plus a small set of scrollback navigation keys, and pass everything else straight through to the running application.
- The Super Key: The default modifier is
Ctrl+A(configurable viaKeyBindings). - Scrollback Keys: Outside of Direct Input Mode, the WM also intercepts
PageUp/PageDown/Home/End(no modifier) for scrollback when a window has scrollback available; arrow keys and other navigation fall through to the child application. - Command Palette: Press
Ctrl+Ato open the central Command Palette overlay. This fuzzy-searchable menu (powered bynucleowith exponential decay scoring for recency) is the primary method for executing actions, opening windows, altering layouts, and managing workspaces (New Workspace, Switch to Workspace:<name>, Detach Viewer). - Window Navigation: While the palette is open, press
TaborShift+Tabto instantly cycle focus between active windows. PressEnterto activate the selected command. - Key Passthrough: Pressing
Ctrl+Awhile the palette is already open immediately sends theCtrl+Akeystroke to the focused child application (SendSuperKeyToFocusedWindow).
term-wm features zero-configuration input routing. Direct Input Mode is automatic. Driven by the DirectInputTracker, term-wm continuously monitors the PTY state. When a child application (such as vim, emacs, or tmux) requests the alternate screen buffer, enables mouse tracking, or defines custom scroll margins, the window manager automatically steps out of the way.
The routing decision is a structured DirectInputMode snapshot with independent keyboard and mouse dimensions:
- Keyboard direct (alternate screen / custom margins): all keystrokes pass through to the application unfiltered — zero-delay, unbuffered pass-through. Native scrollback navigation is suspended.
- Mouse capture (app requested mouse tracking): mouse events are encoded and forwarded to the application. Native text selection is suspended only while the app holds the mouse. An app on the alternate screen that did not request mouse tracking (e.g.
pico/nano) keeps native click-and-drag text selection and wheel scrolling.
A brief notification toast appears on transitions and shows the window's combined access, coalescing rapid sub-mode shifts into one message (e.g. Direct Input Mode (keyboard and mouse) enabled for vim, Direct Input Mode (keyboard) enabled for nano). The Ctrl+A Super Key remains active to summon the Command Palette at any time.
To force native text selection inside an app that captured the mouse, hold Shift (or Option on macOS) while clicking and dragging. This is best-effort: it applies to SGR mouse streams that reach term-wm — when running nested inside a host terminal emulator, the host intercepts Shift+mouse first and performs its own selection.
Floating windows support mouse-driven snapping with a live ghost preview. While dragging a window by its title bar, hovering over a snap target shows a dashed outline and a label describing the pending action.
- Snap targets: screen edges (
snap to edge), screen corners (snap to corner), and the top edge (maximize). - Auto-snap countdown: if the pointer leaves the screen area while a snap target is active, the window snaps automatically after a short countdown (default 2 seconds, configurable via
drag_snap_timeout). Releasing the button over the target also snaps immediately. - Micro-positioning: to place a window at a precise position, float it first, move it where you want, then tile it.
term-wm initially began as a distinct application before its underlying rendering and window management mechanics were extracted into a general-purpose multiplexer. Because the system is built as a collection of decoupled crates, its core layout engine and UI components can theoretically be embedded into other Ratatui applications.
However, the developer-facing library API is currently unsolidified and subject to rapid breaking changes. Stabilizing the developer API, refining the component lifecycle, and documenting the embedded layout engine will be the primary focus of future architectural iterations. (For a glimpse into the internal component design standards, see AGENTS.md).
term-wm ships a "dumb" view! macro that builds component trees declaratively — it expands to ordinary, fully-monomorphized component constructors, with no runtime tree, reactivity, or reconciliation:
use term_wm::prelude::*;
struct MyWindow;
impl MyWindow {
fn view(&mut self) -> impl Component<TermWmAction> + '_ {
view! {
<VStack gap=1>
<Label text="System Status" />
<Button label="Refresh" action={TermWmAction::Quit} />
</VStack>
}
}
}Layout tags (VStack, HStack, Grid, Center, Box) and stateless leaves (Label, Button) are constructed declaratively; a { expr } escape hatch injects any Component value, owned or &mut-borrowed ({ &mut self.terminal } for stateful components such as a terminal). All-owned trees (no &mut) go straight into open_window(AppRootComponent::Custom(view!{..})); borrowed trees use the fn view(&mut self) -> impl Component + '_ pattern above.
view! and its tag set are still an evolving draft — treat examples/view_macro_prototype.rs as the canonical runnable reference (it wires a live terminal into a view! tree), and the System Panel (ToggleSystemPanel) is itself a scrolling view! grid built the same way.
term-wm is primarily distributed under the terms of both the MIT license and the Apache License (Version 2.0).
See LICENSE-APACHE and LICENSE-MIT for details.

