# Developer Integration Guide
This guide is for developers who want to build tools, scripts, IDE extensions, or automation pipelines that use psmux on Windows, especially if you already have tmux integrations on Linux/macOS.
## Why psmux for Developers
psmux implements the same CLI protocol and command set as tmux. If your project already integrates with tmux via subprocess calls, control mode, or libraries like libtmux, you can run on Windows with minimal or zero code changes.
Key points:
- **Same binary name**: psmux installs `tmux.exe` as an alias. Existing scripts that call `tmux` will find psmux on the PATH.
- **Same commands**: 83 tmux commands with the same flags, arguments, and output formats.
- **Same IDs**: `$N` (session), `@N` (window), `%N` (pane) stable IDs follow the tmux scheme.
- **Same control mode**: `-C`/`-CC` wire protocol with `%begin`/`%end` framing, notifications, and output escaping.
- **Same config**: Reads `~/.tmux.conf` directly. Your config, key bindings, and themes transfer as-is.
- **Same format engine**: 140+ format variables with conditionals, loops, regex, and string operations.
## Installation
```powershell
# Cargo (recommended for developers)
cargo install --git https://github.com/psmux/psmux
# Scoop
scoop bucket add extras
scoop install psmux
# Winget
winget install psmux
# Chocolatey
choco install psmux
```
After installation, `psmux`, `pmux`, and `tmux` are all available as commands. Use whichever fits your project.
## Quick Start: Subprocess Integration
The simplest integration pattern. Works with any language that can spawn processes.
### Python
```python
import subprocess
import platform
def mux_cmd(args, encoding="utf-8"):
"""Run a tmux/psmux command and return stdout."""
kwargs = {"capture_output": True, "text": True}
if platform.system() == "Windows":
kwargs["encoding"] = encoding
result = subprocess.run(["tmux"] + args, **kwargs)
if result.returncode != 0:
raise RuntimeError(f"tmux command failed: {result.stderr}")
return result.stdout.strip()
# Create a session
mux_cmd(["new-session", "-d", "-s", "dev", "-x", "120", "-y", "30"])
# Send a command
mux_cmd(["send-keys", "-t", "dev", "echo hello", "Enter"])
# Read pane output
content = mux_cmd(["capture-pane", "-t", "dev", "-p"])
print(content)
# Query format variables
pane_path = mux_cmd(["display-message", "-t", "dev", "-p", "#{pane_current_path}"])
# List all sessions
sessions = mux_cmd(["list-sessions", "-F", "#{session_name}"])
# Clean up
mux_cmd(["kill-session", "-t", "dev"])
```
### PowerShell
```powershell
function Invoke-Mux {
param([string[]]$Args)
$result = & tmux @Args 2>&1
if ($LASTEXITCODE -ne 0) { throw "tmux command failed: $result" }
return $result
}
# Create and interact with a session
Invoke-Mux new-session -d -s dev -x 120 -y 30
$content = Invoke-Mux capture-pane -t dev -p
Write-Host $content
Invoke-Mux kill-session -t dev
```
### Node.js
```javascript
const { execFileSync } = require("child_process");
function muxCmd(args) {
return execFileSync("tmux", args, { encoding: "utf-8" }).trim();
}
// Create a session
muxCmd(["new-session", "-d", "-s", "dev", "-x", "120", "-y", "30"]);
// Send keys
muxCmd(["send-keys", "-t", "dev", "echo hello", "Enter"]);
// Capture output
const content = muxCmd(["capture-pane", "-t", "dev", "-p"]);
console.log(content);
// Clean up
muxCmd(["kill-session", "-t", "dev"]);
```
### Go
```go
package main
import (
"fmt"
"os/exec"
"strings"
)
func muxCmd(args ...string) (string, error) {
out, err := exec.Command("tmux", args...).Output()
return strings.TrimSpace(string(out)), err
}
func main() {
muxCmd("new-session", "-d", "-s", "dev", "-x", "120", "-y", "30")
muxCmd("send-keys", "-t", "dev", "echo hello", "Enter")
content, _ := muxCmd("capture-pane", "-t", "dev", "-p")
fmt.Println(content)
muxCmd("kill-session", "-t", "dev")
}
```
### Rust
```rust
use std::process::Command;
fn mux_cmd(args: &[&str]) -> String {
let output = Command::new("tmux")
.args(args)
.output()
.expect("failed to run tmux/psmux");
String::from_utf8_lossy(&output.stdout).trim().to_string()
}
fn main() {
mux_cmd(&["new-session", "-d", "-s", "dev", "-x", "120", "-y", "30"]);
mux_cmd(&["send-keys", "-t", "dev", "echo hello", "Enter"]);
let content = mux_cmd(&["capture-pane", "-t", "dev", "-p"]);
println!("{}", content);
mux_cmd(&["kill-session", "-t", "dev"]);
}
```
## libtmux Integration
[libtmux](https://github.com/tmux-python/libtmux) is the most popular Python library for controlling tmux programmatically. psmux is compatible with libtmux because it implements the same commands and output formats.
### Setup
```powershell
pip install libtmux
```
### Basic Usage
```python
import libtmux
# Connect to the psmux server
server = libtmux.Server(socket_name="default")
# List sessions
for session in server.sessions:
print(f"{session.name} ({session.id}): {len(session.windows)} windows")
# Work with a session
session = server.sessions[0]
# Create a window
window = session.new_window(window_name="build")
# Access panes
pane = window.panes[0]
# Send commands
pane.send_keys("cargo build")
# Capture output
lines = pane.capture_pane()
for line in lines:
print(line)
# Kill the window
window.kill()
```
### Windows Encoding Fix
libtmux uses the Unicode character U+241E (SYMBOL FOR RECORD SEPARATOR) internally to split format fields when querying tmux. On Linux, this works transparently because both tmux and Python use UTF-8.
On Windows, Python's `subprocess.Popen(text=True)` defaults to cp1252 encoding, which garbles the 3-byte UTF-8 sequence for U+241E. This causes `server.sessions` and similar queries to return empty results or parse errors.
**Option 1**: Set `PYTHONUTF8=1` before running your script:
```powershell
$env:PYTHONUTF8 = "1"
python my_script.py
```
**Option 2**: Patch libtmux locally. In your installed libtmux package, edit `common.py` and add `encoding="utf-8"` to the `Popen` call in the `tmux_cmd.__init__` method:
```python
subprocess.Popen(
cmd, stdout=PIPE, stderr=PIPE, text=True,
encoding="utf-8", errors="backslashreplace"
)
```
This is an upstream libtmux issue (not psmux-specific). The library should specify encoding explicitly for cross-platform compatibility.
### libtmux API Coverage
The following libtmux operations are verified working with psmux:
| `Server(socket_name="default")` | Works | Connects to the running psmux server |
| `server.sessions` | Works | Returns all sessions (needs encoding fix on Windows) |
| `session.id` (`$N`) | Works | Returns the stable session ID |
| `session.windows` | Works | Lists all windows in the session |
| `session.new_window()` | Works | Creates a new window |
| `window.id` (`@N`) | Works | Returns the stable window ID |
| `window.panes` | Works | Lists all panes in the window |
| `pane.id` (`%N`) | Works | Returns the stable pane ID |
| `pane.send_keys()` | Works | Sends keystrokes to the pane |
| `pane.capture_pane()` | Works | Captures visible pane content |
| `window.kill()` | Works | Destroys the window |
| `session.kill()` | Works | Destroys the session |
| `server.has_session()` | Works | Checks if a session exists |
| Custom format queries (`-F`) | Works | All 140+ format variables supported |
## Control Mode Integration
For persistent, event-driven integration (IDE plugins, session managers, monitoring tools), use control mode. See [control-mode.md](control-mode.md) for the full protocol reference.
### Quick Example
```python
import subprocess
import threading
proc = subprocess.Popen(
["psmux", "-CC"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
)
def reader():
for line in proc.stdout:
line = line.rstrip("\n")
if line.startswith("%output"):
_, pane_id, *data = line.split(" ", 2)
print(f"[{pane_id}] {data[0] if data else ''}")
elif line.startswith("%window-add"):
print(f"Window created: {line}")
elif line.startswith("%session-changed"):
print(f"Session changed: {line}")
t = threading.Thread(target=reader, daemon=True)
t.start()
# Send commands
proc.stdin.write("list-windows\n")
proc.stdin.flush()
proc.stdin.write("new-window -n monitor\n")
proc.stdin.flush()
proc.stdin.write('send-keys "Get-Process" Enter\n')
proc.stdin.flush()
```
### psmux Extension Commands
In addition to the 83 standard tmux commands, psmux provides extra commands useful for rich integrations:
| `dump-state` | Full session state as JSON (windows, panes, options, screen content) |
| `dump-layout` | Pane layout tree structure |
| `list-tree` | Hierarchical session/window/pane tree |
| `send-text <text>` | Send raw text to active pane (no key name parsing) |
| `send-paste <text>` | Send text as a bracketed paste sequence |
| `claim-session` | Claim a warm (pre-spawned) session for instant startup |
| `set-pane-title <title>` | Set pane title directly |
| `toggle-sync` | Toggle synchronized input for all panes in a window |
## Named Paste Buffers
psmux supports named paste buffers for structured inter-pane data exchange:
```powershell
# Set a named buffer
psmux set-buffer -b config "key=value"
# Read it from another pane or script
psmux show-buffer -b config
# Delete when done
psmux delete-buffer -b config
# Paste into the active pane
psmux paste-buffer -b config
```
Named buffers are useful for passing structured data between automation steps without relying on environment variables or temporary files.
## Cross-Platform Project Structure
For projects that need to work on both Linux/macOS (tmux) and Windows (psmux), here is a recommended pattern:
### 1. Use the `tmux` Binary Name
psmux installs `tmux.exe` as an alias. Your code can call `tmux` on all platforms:
```python
binary = "tmux" # Works on Linux (real tmux) and Windows (psmux alias)
```
### 2. Set Encoding on Windows
The only platform-specific code you need:
```python
import platform
def get_mux_kwargs():
kwargs = {"capture_output": True, "text": True}
if platform.system() == "Windows":
kwargs["encoding"] = "utf-8"
return kwargs
```
### 3. Handle Path Separators
tmux uses Unix paths (`/home/user/project`), psmux uses Windows paths (`C:\Users\user\project`). Format variables like `#{pane_current_path}` return the native path format. If your code compares paths, normalize them:
```python
from pathlib import Path
pane_path = Path(mux_cmd(["display-message", "-p", "#{pane_current_path}"]))
```
### 4. Shell Differences
On Linux, the default shell in tmux is usually `bash` or `zsh`. On Windows, psmux defaults to PowerShell 7 (`pwsh`). Keep this in mind when sending commands:
```python
import platform
if platform.system() == "Windows":
mux_cmd(["send-keys", "-t", target, "Get-ChildItem", "Enter"])
else:
mux_cmd(["send-keys", "-t", target, "ls -la", "Enter"])
```
### 5. Test Matrix
A typical CI/CD matrix for a cross-platform tmux integration:
```yaml
# GitHub Actions example
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
include:
- os: ubuntu-latest
mux: tmux
- os: windows-latest
mux: psmux
steps:
- name: Install multiplexer
run: |
if [ "${{ matrix.mux }}" = "psmux" ]; then
cargo install --git https://github.com/psmux/psmux
else
sudo apt-get install -y tmux
fi
shell: bash
- name: Run integration tests
run: python -m pytest tests/test_mux_integration.py
env:
PYTHONUTF8: "1"
```
## Environment Variables
psmux sets these environment variables in child processes, matching tmux:
| Variable | Example | Description |
|----------|---------|-------------|
| `TMUX` | `/tmp/tmux-1000/default,12345,0` | Indicates a tmux/psmux session is active |
| `TMUX_PANE` | `%0` | The pane ID of the current pane |
| `TERM` | `xterm-256color` | Terminal type |
| `COLORTERM` | `truecolor` | Indicates 24-bit color support |
Tools that check for `$TMUX` to detect tmux will correctly detect psmux as well.
### Propagating Environment Variables
Use `set-environment` to pass configuration to panes:
```powershell
# Global: all new panes inherit this
psmux set-environment -g API_KEY "sk-..."
# Session-scoped
psmux set-environment PROJECT_ROOT "C:\Projects\myapp"
# On session creation
psmux new-session -s work -e "NODE_ENV=development"
```
## Server Namespaces
Use `-L` to run isolated psmux instances (each with its own sessions, windows, and options):
```powershell
# Create isolated servers for different projects
psmux -L frontend new-session -d -s app
psmux -L backend new-session -d -s api
# Each namespace is completely independent
psmux -L frontend list-sessions # Only shows "app"
psmux -L backend list-sessions # Only shows "api"
# Attach to a specific namespace
psmux -L frontend attach -t app
```
In control mode, the session name includes the namespace:
```powershell
$env:PSMUX_SESSION_NAME = "frontend__app"
psmux -CC
```
The double underscore separates namespace from session name.
## Targeting Syntax Reference
psmux supports the full tmux target syntax for the `-t` flag:
| `mysession` | Session by name |
| `$0` | Session by stable ID |
| `mysession:2` | Window 2 in session "mysession" |
| `mysession:editor` | Window named "editor" in session "mysession" |
| `:2` | Window 2 in the current session |
| `@3` | Window by stable ID |
| `%5` | Pane by stable ID |
| `mysession:2.1` | Pane 1 of window 2 in session "mysession" |
| `.+1` | Next pane |
| `.-1` | Previous pane |
## Hooks for Event-Driven Automation
Hooks let you react to session events without polling:
```powershell
# Run a script when a new window is created
psmux set-hook -g after-new-window "run-shell 'echo window created >> /tmp/events.log'"
# Notify on session attach
psmux set-hook -g client-attached "display-message 'Welcome back!'"
# Auto-layout on split
psmux set-hook -g after-split-window "select-layout tiled"
```
Available hooks: `after-new-session`, `after-new-window`, `after-split-window`, `client-attached`, `client-detached`, `after-select-window`, `after-select-pane`, `after-resize-pane`, `pane-died`, `alert-activity`, `alert-silence`, `alert-bell`, `after-kill-pane`.
## Synchronization with `wait-for`
For multi-step automation that needs coordination between panes:
```powershell
# Pane 1: Wait for a signal
psmux send-keys -t %0 "psmux wait-for ready && echo 'proceeding'" Enter
# Pane 2: Do some work, then signal
psmux send-keys -t %1 "cargo build && psmux wait-for -S ready" Enter
```
`wait-for` supports `-L` (lock), `-S` (signal/unlock), and bare wait. Use it for producer/consumer patterns across panes.
## Troubleshooting
### "no server running" Error
psmux requires a running session. Create one first:
```powershell
psmux new-session -d -s work
```
Or use `has-session` to check:
```powershell
psmux has-session -t work 2>$null
if ($LASTEXITCODE -ne 0) {
psmux new-session -d -s work
}
```
### Empty Results from Format Queries on Windows
If `list-sessions -F`, `list-windows -F`, or `list-panes -F` returns garbled or empty output, your process is decoding psmux's UTF-8 output with the wrong encoding. See the [encoding section](#windows-encoding-fix) above.
### Control Mode Connection Issues
If `psmux -CC` exits immediately, ensure a session exists and `PSMUX_SESSION_NAME` is set:
```powershell
psmux new-session -d -s work
$env:PSMUX_SESSION_NAME = "work"
psmux -CC
```
### ConPTY Differences from Unix PTY
When porting Unix tmux integrations to Windows:
- **Alternate screen buffer**: ConPTY processes SMCUP/RMCUP internally. The `alternate_on` flag is always false in psmux. Use content-based heuristics to detect fullscreen TUI apps.
- **Output normalization**: ConPTY may normalize line endings. `%output` data may differ slightly from Unix tmux output.
- **Ctrl+C**: `GenerateConsoleCtrlEvent` sends to all processes sharing the console. Prefer app-specific quit keys over `C-c` in automation.
- **TUI exit timing**: After a TUI exits, ConPTY needs 4 to 6 seconds to restore the screen. Add a delay before `capture-pane` after TUI exit.
## Related Documentation
- [compatibility.md](compatibility.md) : Full tmux command and feature compatibility matrix
- [control-mode.md](control-mode.md) : Control mode wire protocol reference
- [scripting.md](scripting.md) : Command reference and scripting examples
- [configuration.md](configuration.md) : All options and config file format
- [claude-code.md](claude-code.md) : Claude Code agent team integration
- [features.md](features.md) : Complete feature list