psmux 3.3.4

Terminal multiplexer for Windows - tmux alternative for PowerShell and Windows Terminal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
# Control Mode


Control mode lets external programs drive psmux programmatically over a structured text protocol. Instead of rendering a TUI, psmux sends machine-readable notifications and accepts commands over stdin/stdout, making it the foundation for building plugins, IDE integrations, custom dashboards, session monitors, and any tooling that needs to interact with terminal sessions.

This is the same protocol that tmux uses for its control mode (`tmux -C` / `tmux -CC`), so existing knowledge and many client libraries transfer directly to psmux.

## Quick Start


```powershell
# 1. Create a detached session

psmux new-session -d -s work -x 120 -y 30

# 2. Attach in control mode (no-echo)

psmux -CC
```

psmux connects to the running session and enters a command/response loop. You type commands on stdin, and psmux responds on stdout with structured output.

```
list-windows
%begin 1700000000 1 1
0: pwsh* (1 panes) [120x30]
%end 1700000000 1 1
```

To exit, close stdin (Ctrl+D / EOF) or send `kill-server`.

## Flags


| Flag | Mode | Behavior |
|------|------|----------|
| `-C` | Echo | Commands you send are echoed back to stdout before the response. Useful for debugging and interactive testing. |
| `-CC` | No-echo | Commands are not echoed. This is the mode you want for programmatic use. In this mode, `%exit` is followed by an ST sequence (`ESC \`). |

## Session Targeting


By default, control mode connects to the session stored in `PSMUX_SESSION_NAME`. You can set it before launching:

```powershell
$env:PSMUX_SESSION_NAME = "my-session"
psmux -CC
```

## Wire Protocol


### Command/Response Framing


Every command you send gets a response wrapped in `%begin` / `%end` (or `%error`) markers:

```
<your command>
%begin <timestamp> <command_number> <flags>
<response lines>
%end <timestamp> <command_number> <flags>
```

| Field | Description |
|-------|-------------|
| `timestamp` | Unix epoch seconds when the command was processed |
| `command_number` | Sequential counter (1, 2, 3, ...) for each command in the session |
| `flags` | Reserved, always `1` |

The `%begin` and `%end` lines always share the same timestamp, command number, and flags. If a command fails, the closing frame is `%error` instead of `%end`:

```
nonexistent-command
%begin 1700000000 1 1
unknown command: nonexistent-command
%error 1700000000 1 1
```

Command response blocks never interleave with each other. Notifications (described below) arrive between command blocks, never inside them.

### Notifications


Notifications are asynchronous lines that psmux sends whenever something happens in the session. They always start with `%` and arrive between command response blocks.

#### Window Notifications


| Notification | Meaning |
|---|---|
| `%window-add @<WID>` | A new window was created |
| `%window-close @<WID>` | A window was destroyed |
| `%window-renamed @<WID> <name>` | A window was renamed |
| `%window-pane-changed @<WID> %<PID>` | The active pane in a window changed |
| `%layout-change @<WID> <layout> <visible_layout> <flags>` | A window's pane layout changed (split, resize, etc.) |

#### Session Notifications


| Notification | Meaning |
|---|---|
| `%session-changed $<SID> <name>` | The attached session changed |
| `%session-renamed <name>` | The current session was renamed |
| `%session-window-changed $<SID> @<WID>` | The active window in a session changed |
| `%sessions-changed` | A session was created or destroyed |

#### Pane Output


| Notification | Meaning |
|---|---|
| `%output %<PID> <escaped_data>` | A pane produced output |
| `%pane-mode-changed %<PID>` | A pane entered or exited a special mode (e.g. copy mode) |

#### Flow Control


| Notification | Meaning |
|---|---|
| `%pause %<PID>` | Output for this pane has been paused (client is too far behind) |
| `%continue %<PID>` | Output for this pane has resumed |

#### Client and Buffer


| Notification | Meaning |
|---|---|
| `%client-detached <client>` | A client disconnected from the session |
| `%client-session-changed <client> $<SID> <name>` | Another client changed its attached session |
| `%paste-buffer-changed <name>` | A paste buffer was modified |
| `%paste-buffer-deleted <name>` | A paste buffer was deleted |
| `%message <text>` | A status message was generated (e.g. from `display-message`) |

#### Exit


| Notification | Meaning |
|---|---|
| `%exit` | The control client is disconnecting. In `-CC` mode, followed by `ESC \` (ST sequence). |
| `%exit <reason>` | Disconnecting with a reason (e.g. `too far behind`). |

### ID Formats


All IDs are stable, monotonically increasing integers that never get reused during a server's lifetime:

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

### Output Escaping


Data in `%output` notifications uses octal escaping for non-printable bytes:

| Byte | Encoding |
|------|----------|
| Printable ASCII (0x20 to 0x7E) | Passed through as-is |
| Tab (0x09) | Passed through as-is |
| Backslash (0x5C) | `\\` (doubled) |
| Carriage return (0x0D) | `\015` |
| Line feed (0x0A) | `\012` |
| Any other byte | `\NNN` (3-digit octal) |

Example: `hello\r\n` becomes `%output %0 hello\015\012`.

## Supported Commands


All standard psmux/tmux commands work in control mode. Here are the most useful ones for plugin development:

### Session and Window Management


```
new-window                     # Create a new window
new-window -n editor           # Create a named window
split-window -v                # Split vertically
split-window -h                # Split horizontally
kill-pane                      # Kill the active pane
kill-window                    # Kill the active window
select-window -t 1             # Switch to window 1
select-pane -t %3              # Switch to pane %3
rename-window new-name         # Rename the active window
rename-session new-name        # Rename the session
```

### Querying State


```
list-windows                        # List all windows
list-windows -F '#{window_id}'      # Custom format
list-panes                          # List panes in active window
list-panes -a                       # List all panes across all windows
list-sessions                       # List sessions
list-clients                        # List connected clients
display-message -p '#{pane_id}'     # Print a format variable
has-session -t my-session           # Check if session exists (exit code)
```

### Interacting with Panes


```
send-keys -t %0 "echo hello" Enter  # Send keystrokes to a pane
send-keys -t %0 -l "literal text"   # Send text literally (no key parsing)
capture-pane -t %0 -p               # Capture the visible content of a pane
```

### Configuration and Hooks


```
set-option -g status-style "bg=blue"              # Set an option
show-options -g                                     # Show all global options
set-hook -g after-new-window "display-message hi"  # Set a hook
bind-key M-x display-message "pressed!"            # Bind a key
```

### Server


```
list-commands      # List all available commands
server-info        # Server information
kill-server        # Shut down the server
```

### psmux Extension Commands


These commands are available in psmux but do not exist in tmux:

| Command | Description |
|---|---|
| `dump-state` | Returns the entire session state as a JSON blob (windows, panes, options, sizes, screen content). Invaluable for building rich UIs. |
| `dump-layout` | Returns the pane layout tree structure |
| `list-tree` | Returns a hierarchical session/window/pane tree view |
| `send-text <text>` | Send raw text directly to the 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 faster startup |
| `set-pane-title <title>` | Set the title of the current pane |
| `toggle-sync` | Toggle synchronized input across all panes in a window |

## Building a Plugin


### Minimal Python Example


```python
import subprocess
import threading

proc = subprocess.Popen(
    ["psmux", "-CC"],
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
    text=True,
    env={**__import__("os").environ, "PSMUX_SESSION_NAME": "work"},
)

def read_notifications():
    for line in proc.stdout:
        line = line.rstrip("\n")
        if line.startswith("%output"):
            parts = line.split(" ", 2)
            pane_id = parts[1]
            data = parts[2] if len(parts) > 2 else ""
            print(f"[{pane_id}] {data}")
        elif line.startswith("%window-add"):
            print(f"Window created: {line}")
        elif line.startswith("%begin"):
            pass  # Start of command response
        elif line.startswith("%end"):
            pass  # End of command response
        elif line.startswith("%error"):
            print(f"Command error: {line}")

reader = threading.Thread(target=read_notifications, daemon=True)
reader.start()

# Send a command

proc.stdin.write("list-windows\n")
proc.stdin.flush()

# Create a new window

proc.stdin.write("new-window -n build\n")
proc.stdin.flush()

# Run a command in it

proc.stdin.write('send-keys "cargo build" Enter\n')
proc.stdin.flush()

import time
time.sleep(5)
proc.stdin.close()
proc.wait()
```

### Minimal PowerShell Example


```powershell
$env:PSMUX_SESSION_NAME = "work"
$psi = [System.Diagnostics.ProcessStartInfo]::new()
$psi.FileName = (Get-Command psmux).Source
$psi.Arguments = "-CC"
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.UseShellExecute = $false

$proc = [System.Diagnostics.Process]::Start($psi)

# Send a command

$proc.StandardInput.WriteLine("list-windows")
$proc.StandardInput.Flush()
Start-Sleep -Seconds 1

# Read the response

while ($proc.StandardOutput.Peek() -ge 0) {
    $line = $proc.StandardOutput.ReadLine()
    Write-Host $line
}

$proc.StandardInput.Close()
$proc.WaitForExit(5000)
```

### Minimal Node.js Example


```javascript
const { spawn } = require("child_process");

const proc = spawn("psmux", ["-CC"], {
  env: { ...process.env, PSMUX_SESSION_NAME: "work" },
  stdio: ["pipe", "pipe", "pipe"],
});

proc.stdout.on("data", (chunk) => {
  for (const line of chunk.toString().split("\n")) {
    if (line.startsWith("%output")) {
      const [, paneId, ...rest] = line.split(" ");
      console.log(`[${paneId}] ${rest.join(" ")}`);
    } else if (line.startsWith("%begin")) {
      // Command response starting
    } else if (line.startsWith("%end")) {
      // Command response complete
    }
  }
});

proc.stdin.write("list-windows\n");
proc.stdin.write("new-window -n monitor\n");
proc.stdin.write('send-keys "top" Enter\n');

setTimeout(() => {
  proc.stdin.end();
}, 5000);
```

## Parsing Tips


1. **Read line by line.** Every notification and framing marker is a single line terminated by `\n`.

2. **Track command state.** When you send a command, set a flag. Lines between `%begin` and `%end`/`%error` are the command's output. Everything outside those blocks is asynchronous notifications.

3. **Match begin/end pairs by command number.** The second field in `%begin` and `%end` lines is the command counter. Use it to correlate responses with requests.

4. **Buffer line parsing for `%output`.** Split on the first two spaces: `%output`, pane ID, then the rest is escaped output data.

5. **Decode octal escapes.** Replace `\NNN` sequences in output data with the corresponding byte value. `\134` is a literal backslash.

6. **Handle connection loss gracefully.** If the session dies or the server shuts down, stdout will close (EOF). Your reader loop should exit cleanly.

## Differences from tmux


psmux control mode is wire-compatible with tmux's protocol. A few features that exist in tmux but are not yet implemented in psmux:

| Feature | Status | Notes |
|---------|--------|-------|
| `refresh-client -f` flags | Planned | Per-client flags like `no-output`, `pause-after=N` |
| `refresh-client -A` pane actions | Planned | Per-pane on/off/continue/pause |
| `refresh-client -B` subscriptions | Planned | Filtered format variable monitoring |
| `refresh-client -C WxH` | Planned | Client-side size override |
| `%extended-output` | Planned | Output with age info for flow control |
| `%subscription-changed` | Planned | Subscription value change events |
| Unlinked window notifications | N/A | psmux uses one session per server |

The core protocol (framing, notifications, escaping, IDs, command dispatch) is fully compatible. Plugins targeting the basic tmux control mode protocol will work identically on psmux.

### Windows ConPTY Considerations


If you are porting a Unix tmux plugin to psmux, be aware of these ConPTY behaviors:

- **SMCUP/RMCUP consumed internally.** ConPTY processes alternate screen buffer switches before the output reaches psmux. The `alternate_on` flag is always false. psmux uses a heuristic (last row content analysis) to detect fullscreen TUI applications.
- **Output normalization.** ConPTY may normalize line endings and process certain cursor movement sequences internally. `%output` data may look slightly different from what a Unix tmux session would produce for the same shell command.
- **`capture-pane` always reflects the primary screen buffer.** There is no reliable way to detect whether a pane is showing the alternate screen.
- **Ctrl+C propagation.** `GenerateConsoleCtrlEvent` sends to ALL processes sharing the console, not just the foreground process. When testing TUI apps via `send-keys`, prefer using the app's quit key (e.g. `q`) rather than `C-c`.
- **TUI exit timing.** After a TUI application exits and sends RMCUP, ConPTY needs time to generate the restore sequences. If you `capture-pane` immediately after a TUI exits, you may still see TUI content. Allow 4 to 6 seconds for the screen to settle.

### Namespace Isolation


Use `-L` to run multiple independent psmux servers on the same machine:

```powershell
psmux -L dev new-session -d -s myapp -x 120 -y 30
$env:PSMUX_SESSION_NAME = "dev__myapp"
psmux -CC
```

The `PSMUX_SESSION_NAME` value follows the format `<namespace>__<session>` when using `-L`. The double underscore is the separator.

## Format Variables


Use `display-message -p` to query any format variable:

```
display-message -p '#{session_name}: #{window_index} #{pane_id}'
```

Common variables for control mode plugins:

| Variable | Example | Description |
|----------|---------|-------------|
| `#{session_name}` | `work` | Session name |
| `#{session_id}` | `$0` | Session stable ID |
| `#{window_id}` | `@0` | Window stable ID |
| `#{window_index}` | `0` | Window index |
| `#{window_name}` | `pwsh` | Window name |
| `#{pane_id}` | `%0` | Pane stable ID |
| `#{pane_index}` | `0` | Pane index within window |
| `#{pane_pid}` | `12345` | Pane child process PID |
| `#{pane_current_command}` | `pwsh` | Pane running command |
| `#{pane_width}` | `120` | Pane width in columns |
| `#{pane_height}` | `30` | Pane height in rows |
| `#{cursor_x}` | `5` | Cursor column |
| `#{cursor_y}` | `10` | Cursor row |