Skip to content

Latest commit

 

History

History
425 lines (286 loc) · 16.8 KB

compatibility.md

File metadata and controls

425 lines (286 loc) · 16.8 KB

tmux Compatibility

psmux is the most tmux-compatible terminal multiplexer on Windows.

Overview

Feature Support
Commands 83 tmux commands implemented
Format variables 140+ variables with full modifier support
Config file Reads ~/.tmux.conf directly
Key bindings bind-key/unbind-key with key tables, case-sensitive
Hooks 15+ event hooks (after-new-window, etc.) with set-hook/show-hooks
Status bar Full format engine with conditionals, loops, and multi-line support
Themes 14 style options, 24-bit color, text attributes
Layouts 5 layouts (even-h, even-v, main-h, main-v, tiled)
Copy mode 53 vim keybindings, search, registers, rectangle select
Targets session:window.pane, session:window_name, %id, @id syntax
if-shell / run-shell ✅ Conditional config logic
Paste buffers ✅ Full buffer management
Control mode ✅ -C / -CC programmatic protocol
Popups and menus ✅ display-popup, display-menu
Interactive choosers ✅ choose-tree, choose-buffer, choose-client
Server namespaces ✅ -L for isolated instances
Command chaining ✅ Sequential ; operator
Nesting prevention ✅ Blocks psmux inside psmux
Session environment ✅ set-environment / show-environment

Your existing .tmux.conf works. psmux reads it automatically. Just install and go.

Comparison

psmux Windows Terminal tabs WSL + tmux
Session persist (detach/reattach) ✅ ❌ ⚠️ WSL only
Synchronized panes ✅ ❌ ✅
tmux keybindings ✅ ❌ ✅
Reads .tmux.conf ✅ ❌ ✅
tmux theme support ✅ ❌ ✅
Native Windows shells ✅ ✅ ❌
Full mouse support ✅ ✅ ⚠️ Partial
Zero dependencies ✅ ✅ ❌ (needs WSL)
Scriptable (83 commands) ✅ ❌ ✅
Claude Code agent teams ✅ ❌ ✅
CJK/IME text input ✅ ✅ ✅
Warm session pre-spawn ✅ N/A ❌

Supported Commands

For the full list of supported tmux commands and arguments, see tmux_args_reference.md.

Recent Parity Improvements

This section covers tmux features that were recently brought to full parity.

Case-sensitive Key Bindings

Key bindings now distinguish between lowercase and uppercase letters exactly like tmux. bind-key T binds to Shift+T, while bind-key t binds to lowercase t. This is critical for plugins like PPM (Prefix+I to install) and psmux-sensible (Prefix+R to reload).

Ctrl+Space as Prefix

set -g prefix C-Space now works correctly. Previously, multi-character key names like Space were parsed as single character fallbacks.

Wrapped Directional Pane Navigation

Directional pane navigation (select-pane -U/-D/-L/-R) now wraps at layout edges, matching tmux behavior. Navigating past the rightmost pane wraps to the leftmost, and so on. Wrap is also correctly suppressed while zoomed.

Prefix Repeat Chaining

After pressing the prefix key, successive keypresses within the repeat-time window (default 500ms) each trigger the bound action without needing to re-enter the prefix. This matches tmux's repeat behavior for pane navigation and resize bindings.

Switch Client

switch-client is fully functional with all standard flags (-t, -n, -p, -l). Use it to programmatically switch between sessions.

Window Name Resolution in Targets

Target syntax now resolves window names, not just indices. send-keys -t mysession:mywindow correctly finds the window named "mywindow" in session "mysession".

Manual Rename Flag

new-window -n NAME now sets the manual_rename flag, preventing automatic-rename from overwriting the explicitly specified window name with the foreground process name.

List Commands from Within Session

Commands like list-panes, list-windows, list-clients, list-commands, and show-hooks now work when run from within a psmux session (via Prefix + :). Output is displayed in a temporary overlay.

Source File from Within Session

source-file works from within a live session via Prefix + :. Previously, config changes only took effect after detaching and reattaching or killing the server.

Display Panes Overlay

display-panes (and Prefix + q) now shows pane numbers briefly and auto-dismisses after display-panes-time (default 1s). Type a number during the overlay to switch to that pane.

Hook Deduplication

set-hook -g now replaces existing hooks on reload instead of stacking duplicates. set-hook -gu correctly removes hooks.

Command Chaining with Semicolons

Multiple commands can be chained with ; on a single line, matching tmux behavior:

bind-key M-s split-window -h \; select-pane -L

Run Shell Output

run-shell now displays output in the status bar, matching tmux behavior. Background mode with -b runs fire and forget.

Session Server Persistence

The psmux session server now survives SSH disconnects. On reconnect, sessions are intact and psmux attach reattaches normally.

Bell and Alert Support

BEL characters (\x07) from programs are forwarded to your host terminal for audible beep. The bell-action option controls when bells are forwarded and when the status bar tab gets a bell flag.

Pane Border Labels with Truncation

pane-border-format labels that exceed the pane width are now truncated with ellipsis instead of overflowing or clipping mid-character.

Pane Title Management

select-pane -T "" correctly clears a pane title. The default pane title is the hostname, matching tmux convention. Programs can update the pane title via OSC 0/2 escape sequences (controlled by the allow-set-title option). See pane-titles.md for details on how this interacts with PowerShell and other shells.

Multi-line Status Bar

set -g status 2 enables a multi-line status bar with status-format[0] and status-format[1] fully rendering style directives like #[fg=red], #[align=left], and #[fill=blue].

Status Bar Style Directives

The following inline style directives are now rendered correctly in status-format lines:

  • #[list] for the window list region
  • #[fill=colour] for background fill
  • #[align=left|centre|right] for text alignment
  • #[range=...] for click regions

Format Variable Expansion in Bindings

The -F flag on bind-key now properly expands format variables, enabling plugins like smart-splits.nvim to query pane dimensions.

Set Environment

set-environment and show-environment are fully functional. Environment variables set with set-environment -g are inherited by all new panes at the process level (no shell commands echoed). The new-session -e VAR=val flag also sets session environment correctly.

Unbind All Keys

unbind-key -a correctly removes all key bindings across all key tables. You can also target specific tables: unbind-key -a -T prefix, unbind-key -a -T root, unbind-key -a -T copy-mode.

Client Prefix Format Variable

The #{client_prefix} format variable is correctly set when the prefix key is pressed. This enables status bar indicators like:

set -g status-right "#{?client_prefix,#[bg=red] PREFIX ,}"

Window Zoomed Flag

The #{window_zoomed_flag} format variable is correctly maintained during zoom/unzoom operations.

Capture Pane

capture-pane -p correctly outputs pane content to stdout, enabling scripts and integrations (including Claude Code agent team coordination) to read pane state.

Split Window Percentage

split-window -p <percent> correctly creates splits at the specified percentage instead of defaulting to 50/50.

Split Window Working Directory

split-window -c "#{pane_current_path}" correctly resolves the format variable and opens the new pane in the current pane's working directory.

UTF-8 and CJK Support

Multi-byte UTF-8 characters (box-drawing, emoji, CJK text) render correctly in panes. Pasting CJK text no longer crashes the session. Japanese and Korean IME input is handled with minimal latency (the paste-detection heuristic was tuned to avoid misidentifying rapid IME bursts).

Format Variables

psmux supports 140+ format variables with full modifier support, including:

  • Session/window/pane variables (#S, #W, #P, #{pane_current_path}, etc.)
  • Style and color modifiers
  • Conditional expressions (#{?condition,true,false})
  • Comparison operators (#{==:a,b}, #{!=:a,b}, #{<:a,b})
  • Logical operators (#{||:a,b}, #{&&:a,b})
  • Regex substitution (#{s/pat/rep/:var})
  • String operations: basename (#{b:}), dirname (#{d:}), lowercase (#{l:}), shell quote (#{q:})
  • Truncation and padding (#{=N:var}, #{pN:var})
  • Loop iteration over windows (#{W:fmt}), panes (#{P:fmt}), and sessions (#{S:fmt})

Named Paste Buffers

psmux supports named paste buffers, matching tmux behavior:

# Set a named buffer
psmux set-buffer -b mybuf "hello world"

# Show a named buffer
psmux show-buffer -b mybuf

# Delete a named buffer
psmux delete-buffer -b mybuf

# Paste from a named buffer
psmux paste-buffer -b mybuf

Named buffers are separate from the default (anonymous) buffer stack. They persist for the lifetime of the session and can be used for inter-pane data exchange in scripts and automation workflows.

Developer Integration: Using psmux as a tmux Drop-in on Windows

psmux implements the same CLI protocol as tmux. Any tool, library, or script that drives tmux via subprocess commands will work on psmux with minimal or zero changes. This section covers what developers need to know when integrating.

Same Protocol, Same Commands

psmux accepts the same command syntax as tmux:

# This code works identically with both tmux (Linux/macOS) and psmux (Windows)
import subprocess

def run_mux(cmd):
    binary = "tmux"  # psmux installs a tmux.exe alias
    result = subprocess.run([binary] + cmd, capture_output=True, text=True)
    return result.stdout.strip()

# All of these work on both platforms
run_mux(["new-session", "-d", "-s", "work"])
run_mux(["list-sessions"])
run_mux(["send-keys", "-t", "work", "echo hello", "Enter"])
run_mux(["capture-pane", "-t", "work", "-p"])
run_mux(["list-windows", "-F", "#{window_id}:#{window_name}"])
run_mux(["kill-session", "-t", "work"])

Because psmux installs a tmux.exe alias, existing scripts that call tmux by name will find psmux on the PATH without any binary name changes.

Stable IDs: $N, @N, %N

psmux uses the same stable ID scheme as tmux:

Prefix Entity Example
$ Session $0, $1
@ Window @0, @1, @2
% Pane %0, %1, %2

These IDs are monotonically increasing and never reused during a server's lifetime. Use them for reliable targeting:

# Target by session ID
psmux has-session -t "$0"

# Target by window ID
psmux select-window -t @2

# Target by pane ID
psmux send-keys -t %3 "echo hello" Enter

# Compound targets work too
psmux send-keys -t "$0:@2.%3" "echo hello" Enter

Format Separator Encoding (Windows UTF-8)

Libraries that parse format output from list-sessions -F, list-windows -F, or list-panes -F should be aware of encoding on Windows.

psmux outputs UTF-8 encoded text. On Linux, tmux also outputs UTF-8, and most tools decode correctly because the system locale is UTF-8. On Windows, the default console code page is often cp1252 or cp437, not UTF-8.

If your library uses subprocess.Popen(text=True) in Python without specifying an encoding, Python will use the system default encoding (cp1252 on most Windows systems). This will garble any non-ASCII bytes in the output, including Unicode separator characters like U+241E that some libraries use internally.

Fix: Always specify encoding="utf-8" when reading psmux output:

import subprocess

proc = subprocess.Popen(
    ["psmux", "list-sessions", "-F", "#{session_name}"],
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
    text=True,
    encoding="utf-8",       # Required on Windows
    errors="backslashreplace"
)
stdout, stderr = proc.communicate()

Alternatively, set the PYTHONUTF8=1 environment variable to make Python use UTF-8 everywhere:

$env:PYTHONUTF8 = "1"
python your_script.py

libtmux Compatibility

libtmux is the most popular Python library for programmatically controlling tmux. psmux is compatible with libtmux's API because it implements the same CLI commands and output formats.

Setup

pip install libtmux

Usage

import libtmux

# Connect to the running psmux server
server = libtmux.Server(socket_name="default")

# List sessions
for session in server.sessions:
    print(f"Session: {session.name} (ID: {session.id})")

# Get windows and panes
session = server.sessions[0]
for window in session.windows:
    print(f"  Window: {window.name} (ID: {window.id})")
    for pane in window.panes:
        print(f"    Pane: {pane.id}")

# Create a new window
new_win = session.new_window(window_name="build")

# Send keys to a pane
pane = new_win.panes[0]
pane.send_keys("echo hello from libtmux")

# Capture pane content
output = pane.capture_pane()
print(output)

# Kill the window
new_win.kill()

Windows Encoding Note for libtmux

libtmux internally uses a Unicode separator character (U+241E, SYMBOL FOR RECORD SEPARATOR) to split format query results. On Linux, this works transparently because tmux outputs UTF-8 and Python decodes with UTF-8.

On Windows, libtmux's tmux_cmd class uses subprocess.Popen(text=True) which defaults to cp1252 encoding. The 3-byte UTF-8 sequence for U+241E (0xE2 0x90 0x9E) gets decoded as three separate cp1252 characters, breaking the field parser.

Workaround: Patch libtmux's common.py to add encoding="utf-8" to the Popen call:

# In libtmux/common.py, tmux_cmd.__init__
# Change:
#   subprocess.Popen(cmd, stdout=PIPE, stderr=PIPE, text=True)
# To:
subprocess.Popen(cmd, stdout=PIPE, stderr=PIPE, text=True, encoding="utf-8", errors="backslashreplace")

Or set PYTHONUTF8=1 globally before importing libtmux. This is an upstream libtmux issue (it should specify encoding explicitly for cross-platform support) and not specific to psmux.

Cross-Platform Project Pattern

For projects that need terminal multiplexing on both Linux/macOS (tmux) and Windows (psmux):

import platform
import subprocess

def get_mux_binary():
    """Get the terminal multiplexer binary for the current platform."""
    # psmux installs a tmux.exe alias, so "tmux" works everywhere
    return "tmux"

def mux_run(args, **kwargs):
    """Run a tmux/psmux command portably."""
    binary = get_mux_binary()
    kwargs.setdefault("capture_output", True)
    kwargs.setdefault("text", True)
    if platform.system() == "Windows":
        kwargs.setdefault("encoding", "utf-8")
    return subprocess.run([binary] + args, **kwargs)

def create_session(name, width=120, height=30):
    """Create a detached session."""
    return mux_run(["new-session", "-d", "-s", name, "-x", str(width), "-y", str(height)])

def send_keys(target, keys):
    """Send keys to a target pane."""
    return mux_run(["send-keys", "-t", target] + keys)

def capture_pane(target):
    """Capture pane content."""
    result = mux_run(["capture-pane", "-t", target, "-p"])
    return result.stdout

def list_sessions():
    """List all sessions."""
    result = mux_run(["list-sessions", "-F", "#{session_name}"])
    return result.stdout.strip().split("\n") if result.stdout.strip() else []

def kill_session(name):
    """Kill a session."""
    return mux_run(["kill-session", "-t", name])

This pattern works identically on Linux (with tmux) and Windows (with psmux) because:

  1. psmux installs a tmux.exe alias, so the binary name is the same
  2. The CLI protocol (commands, flags, format strings) is identical
  3. Stable IDs ($N, @N, %N) follow the same scheme
  4. Control mode (-C/-CC) uses the same wire protocol

What About GUI/IDE Integrations?

If you are building an IDE plugin, VS Code extension, or GUI application that manages terminal sessions:

  1. Use control mode (psmux -CC) for persistent, event-driven integration. See control-mode.md.
  2. Use dump-state (psmux extension) to get the full session state as JSON, including screen content.
  3. Query format variables with display-message -p "#{var}" for lightweight state reads.
  4. Set environment variables with set-environment -g KEY val to pass configuration to child processes.
  5. Use hooks (set-hook -g after-new-window ...) to react to session events.
  6. Use wait-for for cross-pane synchronization in multi-step automation.

For a complete developer integration guide with examples in Python, PowerShell, Node.js, and more, see integration.md.