# psmux documentation Generated 2026-10-22 from https://github.com/psmux/psmux/tree/master/docs # Getting Started with psmux: tmux on Windows in 10 Minutes psmux is a native terminal multiplexer for Windows that speaks the tmux command language. This tutorial walks a Windows user from installation to a working, persistent multi-pane session in PowerShell or Windows Terminal, with no WSL, Cygwin or MSYS2 involved. If you have used tmux on Linux or macOS, everything you already know applies; if you have never used a multiplexer, this page is the place to start. **What you will learn** - How to install psmux with winget, Scoop, Chocolatey, Cargo or a release zip - How to start a session, split panes and open windows with the `Ctrl+b` prefix - How to detach, close the terminal window, and reattach later with everything still running - How copy mode, the Windows clipboard and the mouse work - How to write a first `~/.psmux.conf` (your existing `.tmux.conf` also works) - How to pick pwsh, cmd, Git Bash, WSL or nushell as the shell inside panes **Key facts** | | | |---|---| | Binary names | `psmux`, plus `pmux` and `tmux` aliases (type `tmux` and it just works) | | Default prefix | `Ctrl+b`, same as tmux | | Default shell in panes | PowerShell 7 (`pwsh`), configurable per session, window or pane | | Config file | first of `~/.psmux.conf`, `~/.psmuxrc`, `~/.tmux.conf`, `~/.config/psmux/psmux.conf` | | Requirements | Windows 10 or 11; PowerShell 7 recommended | | Terminal | Windows Terminal, plain conhost, VS Code terminal, or any ConPTY host | ## How do I install psmux on Windows? Pick one channel. All of them put `psmux.exe`, `pmux.exe` and `tmux.exe` on your PATH. ```powershell # WinGet (Windows 10/11 built in package manager) winget install psmux # Scoop scoop bucket add psmux https://github.com/psmux/scoop-psmux scoop install psmux # Chocolatey choco install psmux # Cargo (builds from crates.io, needs a Rust toolchain) cargo install psmux ``` You can also download the `.zip` from [GitHub Releases](https://github.com/psmux/psmux/releases) and add the folder to your PATH. Verify the install from any shell: ```powershell psmux -V # psmux 3.3.8 (cbb9c10 2026-08-28) tmux -V # tmux 3.3.8 ``` The `tmux -V` output is deliberately tmux shaped so scripts and tools that check the version string keep working. See the [cross platform scripting tutorial](cross-platform-tmux-scripts.md) for why that matters. psmux launches PowerShell 7 by default. If `pwsh` is not installed it falls back to Windows PowerShell 5, then `cmd.exe`, but the experience is much better with pwsh: ```powershell winget install --id Microsoft.PowerShell ``` ## How do I start my first session? Open Windows Terminal (or any console) and type: ```powershell psmux ``` You get a full screen view with a status bar at the bottom and a PowerShell prompt inside a pane. That prompt is running inside a psmux **server**, a background process that owns your shells. The window you are looking at is a **client** attached to that server. Keeping the two apart is what makes sessions survive later on. Give sessions names from the start; it makes everything else easier: ```powershell psmux new-session -s main ``` `new`, `new-session` and `new -s main` are all the same command. To attach to an existing session, or create it if it does not exist yet: ```powershell psmux new -A -s main ``` ## How do I split a pane in PowerShell? Inside the session, press the prefix (`Ctrl+b`), release it, then press the next key: | Keys | What happens | |------|--------------| | `Ctrl+b` then `%` | Split the current pane left and right | | `Ctrl+b` then `"` | Split the current pane top and bottom | | `Ctrl+b` then arrow key | Move focus to the pane in that direction | | `Ctrl+b` then `o` | Cycle focus to the next pane | | `Ctrl+b` then `z` | Zoom the current pane to full size (press again to restore) | | `Ctrl+b` then `x` | Kill the current pane | | `Ctrl+b` then `Space` | Cycle through the built in layouts | | `Ctrl+b` then `q` | Flash pane numbers, press one to jump there | The same actions are commands, so you can run them from a prompt in another terminal or from a script: ```powershell psmux split-window -h -t main # left/right split in session "main" psmux split-window -v -p 30 -t main # bottom pane takes 30 percent psmux split-window -h -c "#{pane_current_path}" # new pane starts in the same folder psmux select-layout -t main main-vertical ``` Every pane is its own ConPTY with its own shell process. Resizing the terminal window resizes the layout; dragging a pane border with the mouse resizes a split. ## How do I open more windows? A window is a full screen tab with its own set of panes. The status bar lists them. | Keys | What happens | |------|--------------| | `Ctrl+b` then `c` | Create a new window | | `Ctrl+b` then `n` / `p` | Next / previous window | | `Ctrl+b` then `0` to `9` | Jump to window by number | | `Ctrl+b` then `,` | Rename the current window | | `Ctrl+b` then `w` | Interactive window and pane chooser (`choose-tree`) | | `Ctrl+b` then `&` | Kill the current window | From the command line: ```powershell psmux new-window -t main -n logs # named window psmux new-window -t main -n build -- cargo watch # window that runs a command psmux new-window -t main -c C:\Projects\api # window that starts in a folder ``` Clicking a window name in the status bar switches to it. ## How do I detach and reattach a session? This is the feature that makes a multiplexer worth using. Press `Ctrl+b` then `d` to **detach**. Your terminal returns to the plain prompt, but the session, its panes and every process inside them keep running in the psmux server. Close the terminal window entirely. Open a new one. Then: ```powershell psmux ls # main: 3 windows (created Sat Aug 29 15:40:16 2026) psmux attach -t main ``` Everything is exactly where you left it, including a `cargo watch`, an SSH connection or an AI coding agent that was mid task. `psmux ls` (or `list-sessions`) shows what the server is holding, and `psmux kill-session -t main` ends a session for good. Because the server is a separate process, this also works for things started without any window at all: ```powershell # Start a session in the background, no window opens psmux new-session -d -s worker -- python .\long_job.py # Come back later from any terminal psmux attach -t worker ``` The [Windows use cases](../use-cases.md) page builds on this idea for boot time services, remote administration and long running agents. ## How does copy mode and the Windows clipboard work? Press `Ctrl+b` then `[` to enter **copy mode**. The pane border turns yellow, `[copy mode]` appears in the title, and you can move around the scrollback with the arrow keys or `h j k l`, page with `PageUp`/`PageDown`, and search with `/` (forward) and `?` (backward). Press `Space` to start a selection, move the cursor, then `Enter` or `y` to copy it to the Windows clipboard and leave copy mode. `q` or `Escape` leaves without copying. Prefer vi keys? One line of config switches the keymap: ```tmux set -g mode-keys vi ``` One thing to know if you come from vim: as in tmux, `v` toggles rectangle selection and does not start one. `Space` starts a selection and `V` starts a line selection. If you want `v` to begin a selection the way vim does, bind it: ```tmux bind-key -T copy-mode-vi v send-keys -X begin-selection ``` Paste inside a pane with `Ctrl+b` then `]`, or use the terminal's normal paste (`Ctrl+Shift+V` or right click in Windows Terminal); both reach the shell in the active pane. The full copy mode keymap, for emacs and vi, is in [Key Bindings](../keybindings.md). ## Does the mouse work? Yes, and it is on by default. With the mouse you can: - Click a pane to focus it - Click a window name in the status bar to switch to it - Drag a pane border to resize the split - Scroll the wheel to move through scrollback (this enters copy mode at a shell prompt, and passes the wheel through to full screen apps such as `nvim`, `htop` or `less` that ask for it) - Left drag to select text; the selection is copied to the Windows clipboard on release - Right click to paste Applications that draw their own columns (lazygit, opencode, a split nvim) may prefer to handle selection themselves; `set -g mouse-selection off` hands drag selection to the app while keeping clicks, scrolling and border resizing. Details, including how mouse events reach programs over SSH, are in [Mouse and SSH](../mouse-ssh.md) and [Configuration](../configuration.md). ## How do I write my first config file? psmux reads the first file it finds from this list: `~/.psmux.conf`, `~/.psmuxrc`, `~/.tmux.conf`, `~/.config/psmux/psmux.conf`. `~` is your Windows user profile (`C:\Users\`). If you already have a `.tmux.conf` from Linux, copy it over as is; the syntax is tmux syntax. A useful first `~/.psmux.conf`: ```tmux # Use Ctrl+a as the prefix (Ctrl+b is the default) set -g prefix C-a # Windows and panes count from 1 set -g base-index 1 set -g pane-base-index 1 # vi keys in copy mode set -g mode-keys vi # Bigger scrollback set -g history-limit 10000 # Easier splits that keep the current folder bind-key | split-window -h -c "#{pane_current_path}" bind-key - split-window -v -c "#{pane_current_path}" # Reload the config with Prefix + r bind-key r source-file ~/.psmux.conf \; display-message "config reloaded" # Status bar set -g status-style "bg=#1e1e2e,fg=#cdd6f4" set -g status-left "[#S] " set -g status-right "%H:%M %d-%b-%y" ``` Save the file, then either restart psmux or run `psmux source-file ~/.psmux.conf`. To try a config without touching your default one, use `psmux -f C:\path\to\other.conf`, and `psmux -f NUL` starts with no config at all. Themes and plugins (Catppuccin, Dracula, Nord, session save and restore, and more) install through the plugin manager described in [Plugins and Themes](../plugins.md). ## Which shell runs inside the panes: pwsh, cmd, Git Bash, WSL or nushell? The default is PowerShell 7. Change it globally in the config: ```tmux set -g default-shell cmd # cmd.exe set -g default-shell powershell # Windows PowerShell 5 set -g default-shell nu # nushell set -g default-shell wsl # your default WSL distro set -g default-shell "C:/Program Files/Git/bin/bash.exe" # Git Bash ``` Or mix shells in one session, per window or per pane: ```powershell psmux new-window -n linux wsl psmux split-window -h -- "C:/Program Files/Git/bin/bash.exe" psmux new-session -s admin -- cmd ``` A session with pwsh on the left, WSL Ubuntu on the right and a cmd window for legacy tools is a normal psmux setup. [Multi-Shell](../multi-shell.md) covers the bindings, the per shell quirks and how `#{pane_current_path}` behaves in each. ## How do I make Windows Terminal open straight into psmux? Add a profile whose command line attaches to (or creates) a named session. In Windows Terminal open Settings, add a new profile, and set: ```text Command line: psmux new -A -s main Starting directory: %USERPROFILE% ``` Or in `settings.json`: ```json { "name": "psmux", "commandline": "psmux new -A -s main", "startingDirectory": "%USERPROFILE%" } ``` `new -A -s main` attaches if `main` exists and creates it otherwise, so every new tab of that profile lands in the same session, and closing the tab only detaches. Set it as the default profile if you want psmux in every tab. Session creation is fast because psmux keeps a **warm** server pre spawned in the background and claims it on `new-session`; see [Warm Sessions](../warm-sessions.md). ## Where do I go next? - [Windows Use Cases](../use-cases.md): boot time services, remote admin, AI agents, dashboards - [Terminal agents and TUIs](terminal-agents-and-tuis.md): running Claude Code, Codex, lazygit, nvim and friends in panes - [Dev environment layouts](dev-environment-layouts.md): repeatable project layouts and sessionizer scripts - [Cross platform tmux scripts](cross-platform-tmux-scripts.md): automation that runs on Windows, Linux and macOS unchanged - [Key Bindings](../keybindings.md), [Configuration](../configuration.md), [Scripting](../scripting.md), [Command Reference](../tmux_args_reference.md) - [FAQ](../faq.md) and [Compatibility](../compatibility.md) ## FAQ **Q: Do I need WSL, Cygwin or MSYS2 to run psmux?** A: No. psmux is a native Windows executable built on ConPTY. It runs PowerShell, cmd and Windows programs directly. WSL and Git Bash are optional shells you can put inside panes. **Q: Can I type `tmux` instead of `psmux`?** A: Yes. `tmux.exe` and `pmux.exe` ship alongside `psmux.exe` and behave identically, so muscle memory and existing scripts keep working. **Q: My `.tmux.conf` from Linux has lines psmux does not understand. Will it break?** A: No. An unknown option produces a one line warning at startup and is otherwise skipped, so the rest of the file still loads. The [Compatibility](../compatibility.md) matrix lists every supported command and option, and `psmux list-commands` prints the live list for your build. **Q: Where did my session go after I closed Windows Terminal?** A: Nowhere. Closing the window detaches the client. Run `psmux ls` from a new terminal and `psmux attach -t ` to get back in. Only `kill-session`, `kill-server` or a reboot ends a session. **Q: Does psmux work over SSH into a Windows machine?** A: Yes. Install psmux on the Windows host, SSH in with OpenSSH, and run `psmux` there. Mouse support over SSH depends on the Windows build; [Mouse and SSH](../mouse-ssh.md) has the table. **Q: How do I see every key binding currently active?** A: Press `Ctrl+b` then `?`, or run `psmux list-keys`. --- # FAQ Answers to the questions people ask most about psmux, the native tmux for Windows: what it runs on, how it relates to tmux and Windows Terminal, whether an existing `.tmux.conf` carries over, how the mouse and the wheel behave inside programs, and what to do when a key, a colour or a path does not look right. Each answer links to the guide that has the full detail. **Q: Is psmux cross-platform?** A: No. psmux is built exclusively for Windows using the Windows ConPTY API. For Linux/macOS, use tmux. psmux is the Windows counterpart. **Q: Does psmux work with Windows Terminal?** A: Yes! psmux works great with Windows Terminal, PowerShell, cmd.exe, ConEmu, and other Windows terminal emulators. **Q: Why use psmux instead of Windows Terminal tabs?** A: psmux offers session persistence (detach/reattach), synchronized input to multiple panes, full tmux command scripting, hooks, format engine, and tmux-compatible keybindings. Windows Terminal tabs can't do any of that. **Q: Can I use my existing `.tmux.conf`?** A: Yes! psmux reads `~/.tmux.conf` automatically. Most tmux config options, key bindings, and style settings work as-is. **Q: Can I use tmux themes?** A: Yes. psmux supports 20+ style options with 24-bit true color, 256 indexed colors, and text attributes (bold, italic, dim, etc.). Most tmux theme configs are compatible. See the Style Options table in [configuration.md](configuration.md) for the full set. **Q: Can I use tmux commands with psmux?** A: Yes! psmux includes a `tmux` alias. Commands like `tmux new-session`, `tmux attach`, `tmux ls`, `tmux split-window` all work. 90+ commands in total. Run `psmux list-commands` to see exactly what your build accepts. **Q: How fast is psmux?** A: Session creation takes < 100ms. New windows/panes add < 80ms overhead. The bottleneck is your shell's startup time, not psmux. Compiled with opt-level 3 and full LTO. **Q: Does psmux support mouse?** A: Full mouse support: click to focus panes, drag to resize borders, scroll wheel, click status-bar tabs, drag-select text (including tmux-like copy-on-release with `pwsh-mouse-selection on`), and right-click copy/paste paths. Plus VT mouse forwarding for TUI apps like vim, htop, and midnight commander. **Q: When I select lines with the mouse, the selection runs across every pane. How do I copy from just one pane?** A: A selection that spans panes, covers whole rows of the window and runs straight through the pane border is drawn by your terminal (Windows Terminal, conhost, WezTerm), not by psmux. The terminal only sees one big grid of characters and knows nothing about panes, so its selection cannot stop at a pane edge. tmux behaves exactly the same way. You get the terminal's selection in two cases: - `mouse` is off in your config (`set -g mouse off`). psmux then leaves every mouse event to the terminal. psmux turns `mouse` on by default; tmux defaults it to off. - You hold **Shift** while dragging. Windows Terminal and most other terminals treat Shift+drag as "select it myself" even when the program inside asks for the mouse. That is handy for grabbing text from several panes at once, and it is how you get the terminal's selection back while `mouse` is on. To copy from one pane only, let psmux do the selecting: - With the mouse: make sure your config has `set -g mouse on` (or no `mouse` line at all), then drag inside the pane. psmux enters copy mode, keeps the selection inside the pane you started in even if the pointer wanders into the next one, and copies to the paste buffer and the Windows clipboard when you release the button. - With the keyboard: press `Prefix` then `[` to enter copy mode in the active pane, move to the start, press `Space`, move to the end, press `Enter`. Paste with `Prefix` then `]`. Add `set -g mode-keys vi` if you prefer vi movement keys (`h j k l`, `w`, `b`, `/` to search). See [Copy/Scroll Mode](keybindings.md#copyscroll-mode-vi). **Q: The scroll wheel does nothing inside some full screen program. Why?** A: Because that program never asked for the mouse, and psmux follows tmux here. tmux only writes a mouse report to a pane whose application has enabled a mouse mode (`input_key_mouse` in `input-keys.c` returns immediately otherwise), and its default `WheelUpPane` binding treats the alternate screen only as a reason NOT to fall through to copy mode. So over a full screen program with no mouse support the wheel is a no-op in tmux, and now in psmux too. Before psmux 3.3.9 psmux forwarded the report anyway, and a program that does not parse mouse reports read the bytes as keystrokes: htop opened its `Search:` prompt and typed the report into it, and codex lost its transcript ([#598](https://github.com/psmux/psmux/issues/598)). Turn the program's own mouse support on (`:set mouse=a` in vim and neovim, `--mouse` for `less`, the mouse setting in htop) and the wheel starts working again. Over a plain shell prompt the wheel still enters copy mode and scrolls psmux's own scrollback, unchanged. **Q: The mouse works in some programs but not in Claude Code on Windows 10. Why?** A: On Windows builds below 22523 (Windows 10 19041 and 19045, Windows Server 2019 and Server 2022 build 20348) conhost's inbound VT parser does not pass an SGR mouse report to the pane's child. psmux writes that report into the pane, and on those builds it dies inside conhost, so a program that reads the mouse as VT bytes on stdin never sees it. That covers node based TUIs such as Claude Code and anything reading raw stdin. Programs that read the mouse as console INPUT_RECORDs do get it, because psmux also injects a Win32 `MOUSE_EVENT` record straight into the pane's console input buffer, which skips that parser entirely. That covers crossterm and ratatui apps, Bubble Tea and other Go TUIs, PSReadLine, and native Windows TUIs, and since psmux 3.3.9 it covers clicks, releases and drags on those builds and not only the wheel. On 22523 and above both kinds of program work. This was measured on real 19045 hardware in [#597](https://github.com/psmux/psmux/issues/597): a crossterm app received every wheel notch while a node child reading stdin received zero bytes. There is no psmux level fix for the VT half, the data is lost inside conhost. Upgrading to a build of 22523 or later is the only way to get the mouse into a VT reading program. **Q: What shells does psmux support?** A: PowerShell 7 (default), PowerShell 5, cmd.exe, Git Bash, WSL, nushell, and any Windows executable. Change with `set -g default-shell `. Panes get PowerShell 7 even when you started psmux from a Windows PowerShell 5.1 tab, so `$PROFILE` inside a pane is the `Documents\PowerShell\` one, not `Documents\WindowsPowerShell\`. If you want your 5.1 profile and bindings, set `set -g default-shell powershell`, or set the `SHELL` environment variable to `powershell.exe` (psmux seeds `default-shell` from `SHELL` like tmux does). **Q: Is it stable for daily use?** A: Yes. psmux is stress-tested with 15+ rapid windows, 18+ concurrent panes, 5 concurrent sessions, kill+recreate cycles, and sustained load, all with zero hangs or resource leaks. **Q: PSReadLine predictions / intellisense / autocompletion (inline history suggestions) are disabled inside psmux. How do I enable them?** A: Add `set -g allow-predictions on` to your `~/.psmux.conf`. This tells psmux to preserve your `PredictionSource` setting after initialization. If your profile sets `PredictionSource` explicitly, psmux respects that. If not, psmux restores the system default (typically `HistoryAndPlugin`). See the [PSReadLine Predictions](configuration.md#psreadline-predictions-intellisense--autocompletion) section in the configuration docs for details. **Q: How do I use a custom config file?** A: Use the `-f` flag: `psmux -f /path/to/config.conf`. This loads the specified file instead of the default search order. **Q: How do I disable warm (pre-spawned) sessions?** A: Add `set -g warm off` to your config, or set `$env:PSMUX_NO_WARM = "1"`. See [warm-sessions.md](warm-sessions.md) for details. **Q: Can I set environment variables for panes?** A: Yes. Use `psmux set-environment -g VARNAME value` to set env vars inherited by all new panes. Use `-gu` to unset. See [configuration.md](configuration.md) for details. **Q: How do I mute the audible bell inside psmux?** A: Add `set -g bell-action none` to your `~/.psmux.conf`. This silences both the audible beep and the status bar bell flag. To keep the visual flag but mute the sound, this is not currently split into separate controls. See the [Bell](configuration.md#bell) section in the configuration docs. **Q: Does psmux work with Claude Code agent teams?** A: Yes, first-class support. Start psmux, run `claude` inside a pane, and ask Claude to create a team. psmux automatically sets the required environment variables and injects `--teammate-mode tmux`. Each teammate agent gets its own visible pane. See [claude-code.md](claude-code.md) for details. **Q: Do CJK characters (Chinese/Japanese/Korean) and IME input work?** A: Yes. CJK character input, IME composition, and pasting CJK text all work correctly. The paste detection heuristic is tuned to avoid misidentifying rapid IME bursts as clipboard pastes, keeping IME input latency minimal. Emoji and other characters outside the Basic Multilingual Plane (such as the JIS X 0213 kanji `𠮟`) work too, pasted, typed through the IME or picked from the Win+. emoji panel. **Q: Can I save and restore sessions across reboots?** A: Yes, using the [psmux-resurrect](https://github.com/psmux/psmux-plugins/tree/main/psmux-resurrect) plugin. For automatic periodic save/restore, pair it with [psmux-continuum](https://github.com/psmux/psmux-plugins/tree/main/psmux-continuum). See [plugins.md](plugins.md) for setup. **Q: Do sessions survive SSH disconnects?** A: Yes. The psmux session server persists even when your SSH connection drops. After reconnecting, run `psmux attach` to reattach to your sessions. **Q: How do I reload my config without restarting psmux?** A: Press `Prefix + :` to open the command prompt, then type `source-file ~/.psmux.conf`. You can also run `psmux source-file ~/.psmux.conf` from another terminal. Either way, all options, key bindings, and styles are re-applied immediately. To make it a one-key action, bind it: `bind-key R source-file ~/.psmux.conf \; display-message "Config reloaded"`. If a reloaded line seems to have been ignored, check `~/.psmux/config-warnings.log`, described in [diagnostics.md](diagnostics.md). **Q: How do I run commands from inside a psmux session?** A: Press `Prefix + :` to open the command prompt. Type any command (e.g. `split-window -h`, `new-window -n logs`, `set -g status-style "bg=blue"`). You can also run `list-commands` from the prompt to see all available commands. **Q: How do I switch between sessions?** A: Press `Prefix + s` to open the interactive session chooser. Use arrow keys to navigate and Enter to select. You can also use `Prefix + (` and `Prefix + )` to cycle through sessions, or `switch-client -t sessionname` from the command prompt. **Q: How do I split a pane with a specific size?** A: Use the `-p` flag with a percentage: `split-window -v -p 30` gives the new pane 30% of the space. This works with both `-v` (vertical) and `-h` (horizontal) splits. **Q: How do I open a new pane in the same directory?** A: Use `split-window -c "#{pane_current_path}"`. You can bind this in your config for convenience: `bind-key '"' split-window -v -c "#{pane_current_path}"`. **Q: `#{pane_current_path}` does not follow `cd` inside `wsl`. Why?** A: psmux answers `#{pane_current_path}` the way tmux does, by asking the operating system for the working directory of the pane's foreground process. That works for PowerShell, `cmd`, Cygwin bash and Git Bash, because their `cd` also moves the Win32 working directory. It cannot work for WSL: the shell you are typing at is a Linux process inside the WSL VM, and the only Windows processes in the pane are `wsl.exe` and `wslhost.exe`, whose working directory is fixed when they start and never moves. Nothing on the Windows side can see where the Linux shell went. The fix is shell integration: have the Linux shell announce its directory. Add this one line to `~/.bashrc` in the distro: ```bash PROMPT_COMMAND='printf "\033]7;file://%s%s\033\\" "$HOSTNAME" "$PWD"' ``` or for zsh, in `~/.zshrc`: ```zsh precmd() { printf '\033]7;file://%s%s\033\\' "$HOST" "$PWD"; } ``` psmux translates what it receives into a native Windows path, so `/mnt/c/Users` becomes `C:\Users` and a Linux only directory such as `/home/you` becomes `\\wsl.localhost//home/you` (written with backslashes), which `split-window -c` can actually open. ConEmu's `OSC 9;9` form is accepted too, if you already emit that: ```bash PROMPT_COMMAND='printf "\033]9;9;%s\033\\" "$PWD"' ``` This is optional. Without it nothing breaks: `#{pane_current_path}` simply keeps the last directory it could observe, which is where you were before you typed `wsl`. Note that Windows Terminal needs the same shell integration for its own "duplicate tab in the same directory", so many WSL users already have it. **Q: Task Manager shows psmux at "Above normal" priority. Why, and can I turn it off?** A: By design. Windows gives its foreground scheduling boost to the process that owns the foreground window; the psmux server owns no window and the client draws inside a window the terminal host owns, so neither gets it and keystrokes queue behind compute jobs on a loaded machine. psmux therefore runs its own server and client at `above-normal`. Only psmux's processes are raised: the shells and programs inside panes always start at normal priority. Change it with `set -g priority normal` (or `high`) in your config, or per shell with `$env:PSMUX_PRIORITY = "normal"`, which outranks the option. `realtime` is refused. See [Process Priority](configuration.md#process-priority). **Q: `bind ы ...` or `bind M-ф ...` from my tmux config does nothing.** A: It works on current builds. Key names are parsed by character, so any single Unicode character is a valid key with or without `C-`, `M-` and `S-`. A key name psmux cannot parse is now reported rather than dropped: the CLI prints `unknown key: ` and exits 1, a config file line lands in the boot summary and in `~/.psmux/config-warnings.log`. tmux's mouse names (`WheelUpPane` and friends) are real keys: `bind -n WheelUpPane ...` runs on the wheel, `unbind` removes the default, and `list-keys` shows both (see [Mouse key bindings](keybindings.md#mouse-key-bindings)). See [Supported Key Names](keybindings.md#supported-key-names). **Q: Copy mode search cannot find text that has scrolled off screen.** A: It can on current builds. `/`, `?`, `Ctrl+s`, `Ctrl+r` and `n` / `N` walk the whole scrollback, and an off screen match scrolls the view to it the way tmux does. `send-keys -X search-backward ` from a script does the same and updates `#{search_match}`. **Q: How do I prevent psmux from nesting inside itself?** A: psmux automatically detects when it is already running inside a psmux session and prevents accidental nesting. If you try to start `psmux` inside an existing session, it will warn you instead of creating a nested instance. To explicitly create a new session from within psmux, use the command prompt (`Prefix + :`) and type `new-session`. **Q: How do I keep a pane open after its process exits?** A: Add `set -g remain-on-exit on` to your config. When a process exits, the pane stays visible with its last output. Use `respawn-pane` (or `respawn-pane -k`) to restart the process in that pane. **Q: How do I make pane numbers start from 1 instead of 0?** A: Add `set -g pane-base-index 1` to your config. This affects the `Prefix + q` display panes overlay and pane target numbering. For windows, use `set -g base-index 1`. **Q: How do I set a window name that does not get overwritten?** A: Use the `-n` flag when creating: `new-window -n "myname"`. This automatically disables `automatic-rename` for that window. If you renamed a window with `Prefix + ,` and it keeps getting overwritten, add `set -g automatic-rename off` to your config or set it per-window with `set -w automatic-rename off`. **Q: How do I use PSReadLine ListView (dropdown suggestions) inside psmux?** A: First, add `set -g allow-predictions on` to your `~/.psmux.conf`. Then in your PowerShell profile, set `Set-PSReadLineOption -PredictionViewStyle ListView`. Without `allow-predictions on`, psmux resets PSReadLine settings during initialization. **Q: How do I get a live updating clock in my status bar?** A: Use time format variables like `%H:%M:%S` in your status-right: `set -g status-right "%H:%M:%S %d-%b-%y"`. Then set `set -g status-interval 1` to refresh every second. **Q: What is the difference between psmux, pmux, and tmux executables?** A: They are all the same binary. psmux installs as `psmux.exe` with `pmux.exe` and `tmux.exe` as aliases. Use whichever name you prefer. The `tmux` alias lets existing tmux scripts and muscle memory work without changes. **Q: Can I prevent psmux from entering copy mode on mouse scroll?** A: Yes. Add `set -g scroll-enter-copy-mode off` to your config. Scroll events will be passed directly to the running application instead of entering copy mode. **Q: Ctrl+V is intercepted by psmux even after unbinding. How do I let Ctrl+V reach neovim/vim?** A: First check the terminal: Windows Terminal pastes on Ctrl+V by default and then the key never reaches psmux or neovim at all. Remove its `ctrl+v` paste action (or bind paste to Ctrl+Shift+V only). Once the terminal does not paste on Ctrl+V, psmux treats the key like tmux does: a `bind -n C-v` binding runs, and with no binding the pane gets C-v, so neovim enters visual block mode. Older psmux versions needed `set -g paste-detection off` for this; it is no longer required. You can still paste using Ctrl+Shift+V, right click, or Prefix + ]. See [configuration.md](configuration.md#paste-detection-ctrlv-passthrough) for details. **Q: How do I chain multiple commands in a key binding?** A: Use `\;` to separate commands: `bind-key M-s split-window -h \; select-pane -L`. The semicolon must be escaped in config files. **Q: Can I run psmux inside psmux (nested sessions)?** A: No. psmux prevents nesting to avoid UI confusion. This matches tmux behavior. If you need to connect to a remote psmux, use SSH from within a psmux pane to reach the remote session. **Q: How do I use Ctrl+Space as my prefix key?** A: Add to your config: `set -g prefix C-Space` followed by `unbind-key C-b` and `bind-key C-Space send-prefix`. **Q: Why does `Prefix + I` not work for plugin install?** A: Make sure you are pressing `Shift+I` (uppercase). Key bindings are case-sensitive: `I` and `i` are distinct bindings. **Q: Does psmux work with Neovim/Vim?** A: Yes. Ctrl+[, Shift+Tab, mouse events, and truecolor rendering all work correctly inside psmux panes. Set `set -g default-terminal "xterm-256color"` for best compatibility. **Q: Why does my status bar show a file path instead of the hostname?** A: PowerShell 7 automatically sets the terminal title to the current working directory on every prompt. If your config has `set -g allow-set-title on` and your status bar format uses `#{pane_title}` or `#T`, you will see that path. By default, `allow-set-title` is `off` in psmux so this does not happen. If you enabled it and want to revert, remove the `allow-set-title on` line from your config, replace `#{pane_title}` with `#H` in your status bar format, or add `$PSStyle.WindowTitle = ''` to your PowerShell profile. See [pane-titles.md](pane-titles.md) for full details. **Q: Can I run multiple isolated psmux servers?** A: Yes, use the `-L` flag for server namespaces: `psmux -L work new-session -s dev`. Each namespace gets its own server, sessions, and discovery files. **Q: How many tmux commands does psmux support?** A: 90+ tmux-compatible commands, covering session management, window and pane control, copy mode, popups and menus, interactive choosers, hooks, environment variables, pipe-pane, wait-for synchronization, and more. Rather than trusting a number in a doc, run `psmux list-commands`, which prints exactly what your build accepts. See [tmux_args_reference.md](tmux_args_reference.md) for per-command flags. **Q: psmux is misbehaving. Where are the logs?** A: Under `%USERPROFILE%\.psmux\`. Three files are written without you doing anything: `server-startup.log` (why a server failed to start, including the real Windows error and the path it tried to spawn), `config-warnings.log` (config lines that were silently ignored), and `crash.log` (a panic plus backtrace). Eleven more detailed loggers can be switched on one environment variable at a time, for example `$env:PSMUX_INPUT_DEBUG = "1"`. See [diagnostics.md](diagnostics.md) for the full list and for what to attach to a bug report. --- ## Developer Integration FAQ **Q: Can I use psmux as a drop-in replacement for tmux in my project?** A: Yes. psmux implements the same CLI protocol, commands, flags, and output formats as tmux. It also installs a `tmux.exe` alias, so scripts calling `tmux` will find psmux on the PATH without any code changes. **Q: Does libtmux work with psmux?** A: Yes. libtmux (the Python tmux API library) works with psmux because psmux implements the same commands and output formats. On Windows, you need to ensure UTF-8 encoding is used (set `PYTHONUTF8=1` or patch libtmux's `common.py` to add `encoding="utf-8"` to the Popen call). See [integration.md](integration.md) for details. **Q: Why does libtmux return empty sessions on Windows?** A: libtmux uses a Unicode separator character (U+241E) internally to parse format output. On Windows, Python defaults to cp1252 encoding which garbles this character. Set `$env:PYTHONUTF8 = "1"` before running your script, or patch libtmux to use `encoding="utf-8"`. This is an upstream libtmux issue, not psmux-specific. **Q: Does psmux support control mode for IDE integrations?** A: Yes. `psmux -CC` enters control mode with the same wire protocol as tmux (command/response framing with `%begin`/`%end`, async notifications for window/session/pane events, output escaping). See [control-mode.md](control-mode.md) for the full protocol reference. **Q: What is `dump-state` and when should I use it?** A: `dump-state` is a psmux extension command (not in tmux) that returns the entire session state as a JSON blob, including windows, panes, options, sizes, and screen content. Use it when building rich UIs or debugging integrations. **Q: Do named paste buffers work?** A: Yes. `set-buffer -b "text"`, `show-buffer -b `, `paste-buffer -b `, and `delete-buffer -b ` all work. Named buffers are useful for structured data exchange between automation steps. **Q: How do I handle encoding when reading psmux output in Python?** A: Always specify `encoding="utf-8"` in `subprocess.Popen()` or `subprocess.run()` calls on Windows. Alternatively, set the `PYTHONUTF8=1` environment variable globally. psmux outputs UTF-8, but Python defaults to cp1252 on Windows. **Q: Can I target windows by their stable ID (`@N`) instead of index?** A: Yes. `psmux select-window -t @2` targets the window with stable ID 2 (not display index 2). Stable IDs are assigned when windows are created and never change during the server's lifetime. **Q: What environment variables does psmux set?** A: `TMUX` (session info), `TMUX_PANE` (pane ID like `%0`), `TERM=xterm-256color`, and `COLORTERM=truecolor`. Tools that check `$TMUX` to detect tmux will correctly detect psmux. **Q: Where is the full developer integration guide?** A: See [integration.md](integration.md) for examples in Python, PowerShell, Node.js, Go, and Rust, plus cross-platform project patterns, libtmux usage, control mode integration, and troubleshooting. --- # Features psmux brings the tmux feature set to Windows natively: sessions that outlive the terminal window, split panes and windows, full mouse support, a vim style copy mode, tmux themes and plugins, 90+ tmux compatible commands and 140+ format variables, all in one Rust binary on ConPTY with no WSL, Cygwin or MSYS2. This page is the complete list; the [Getting Started tutorial](tutorials/getting-started-windows.md) shows the essentials in ten minutes. ## Highlights - 🦠 **Made in Rust** : opt-level 3, full LTO, single codegen unit. Maximum performance. - 🖱️ **Full mouse support** : click panes, drag-resize borders, scroll, click tabs, select text, right-click copy - 🎨 **tmux theme support** : 16 named colors + 256 indexed + 24-bit true color (`#RRGGBB`), 20+ style options - 📋 **Reads your `.tmux.conf`** : drop-in config compatibility, zero learning curve - ⚡ **Blazing fast startup** : sub-100ms session creation, near-zero overhead over shell startup - 🔌 **90+ tmux-compatible commands** : `bind-key`, `set-option`, `if-shell`, `run-shell`, `display-popup`, `display-menu`, hooks, and more. Run `psmux list-commands` for the live list - 🪟 **Windows-native** : ConPTY, Win32 API, works with PowerShell, cmd, bash, WSL, nushell - 📦 **Single binary, no dependencies** : install via `cargo`, `winget`, `scoop`, or `choco` - 🤖 **Claude Code agent teams** : first-class support for teammate pane spawning - 🌐 **CJK/IME input** : full support for Chinese, Japanese, and Korean input methods ## Terminal Multiplexing - Split panes horizontally (`Prefix + %`) and vertically (`Prefix + "`) - Multiple windows with clickable status-bar tabs - Session management: detach (`Prefix + d`) and reattach from anywhere - 5 layouts: even-horizontal, even-vertical, main-horizontal, main-vertical, tiled - **Floating panes** : `new-pane` (`newp`) creates a pane that floats above the tiled layout, positioned with `-X`/`-Y`, sized with `-x`/`-y`, titled with `-T`, bordered with `-B`, and movable and resizable with the mouse. See [scripting.md](scripting.md) - **Cross-session pane transfer** : `join-pane -s othersession:1.0` and `move-pane` move a live pane between two independent session servers. The real console stays put and its input and output are tunneled, so the running program never notices. See [scripting.md](scripting.md) ## Full Mouse Support - **Click** any pane to focus it, input goes to the right shell - **Drag** pane borders to resize splits interactively - **Click** status-bar tabs to switch windows - **Scroll wheel** in any pane, scrolls that pane's output (configurable via `scroll-enter-copy-mode`) - **Drag-select** text to copy to clipboard - **Drag auto-scroll** : dragging a selection past the top of a pane continues it into scrollback via copy mode, and holding the pointer on the pane's first or last row keeps scrolling one line every 50ms, like tmux; psmux additionally keeps scrolling, faster, when the pointer moves past the edge. Output taller than the window can be selected in one gesture. With `scroll-enter-copy-mode off`, a selection made over a direct-scrolled view also continues past the bottom edge, back toward the live output - **Right-click** to paste or copy selection - **tmux-like release copy selection** : pane-clipped drag copy on left-button release with word/line multi-click (`pwsh-mouse-selection on`) - **Disable client-side selection** : let in-pane TUI apps (opencode, lazygit, etc.) handle their own mouse selection (`mouse-selection off`) - **VT mouse forwarding** : apps like vim, htop, and midnight commander get full mouse events - **3-layer mouse injection** : VT protocol, VT bridge (for WSL/SSH), and native Win32 MOUSE_EVENT - **Mouse over SSH** : works from any OS client when server runs Windows 11 build 22523+ - **Disable mouse** : `set -g mouse off` fully suppresses mouse event handling ## tmux Theme & Style Support - **20+ customizable style options** : status bar, pane borders, messages, copy-mode highlights, popups, menus, window backgrounds. See [configuration.md](configuration.md) for the full table - **Full color spectrum** : 16 named colors, 256 indexed (`colour0` to `colour255`), 24-bit true color (`#RRGGBB`) - **Text attributes** : bold, dim, italic, underline, blink, reverse, strikethrough, and more - **Styled underscores** : double, curly (undercurl), dotted and dashed underlines from `ESC[4:2m` through `ESC[4:5m`, plus the SGR 58 underline colour, are parsed and passed on to the outer terminal - **Status bar** : fully customizable left/right content with format variables - **Window tab styling** : separate styles for active, inactive, activity, bell, and last-used tabs - **Pane border line styles** : `set -g pane-border-lines double` picks the glyph set (`single`, `double`, `heavy`, `simple`, `number`, `spaces`, `none`), and a post-pass upgrades straight runs to proper junction glyphs where borders meet. See [configuration.md](configuration.md) - Compatible with existing tmux theme configs ## Copy Mode (Vim Keybindings) - **53 vi-style key bindings** : motions, selections, search, text objects - Visual, line, and **rectangle selection** modes (`v`, `V`, `Ctrl+v`) - `/` and `?` search with `n`/`N` navigation, across the whole scrollback (an off screen match scrolls the view to it) - Copy mode is a property of the pane, as in tmux: new panes start live, and more than one pane can be in copy mode at once - **The view is a snapshot, as in tmux**: entering copy mode freezes the pane's grid, so output that keeps arriving cannot push the view down or evict the lines under the cursor. Every pane in copy mode holds its own snapshot for as long as it is in copy mode, focused or not, the way a tmux pane keeps the mode it was put into. The live screen keeps running behind it, `capture-pane` (plain, `-S`/`-E`, `-e`) still answers with the live screen, and leaving copy mode reveals everything that arrived. `#{history_size}` keeps reporting the live grid while copy mode is active, as tmux does, so a status line pairing it with `#{scroll_position}` describes the live pane rather than the frozen view. The snapshot shares the pane's retained history instead of copying it, so opening copy mode on a 50000 line scrollback costs under a megabyte rather than the whole history - `f`/`F`/`t`/`T` character find, `%` bracket matching, `{`/`}` paragraph jump - Named registers (`"a` through `"z`), count prefixes, word/WORD variants - Mouse drag-select copies to Windows clipboard on release and returns to the live view (tmux parity); dragging at the pane's first/last row auto-scrolls through scrollback - **Line numbers** : `set -g copy-mode-line-numbers relative` adds a left gutter, with `off`, `default`, `absolute`, `relative`, and `hybrid` modes and independent styling through `copy-mode-line-number-style` and `copy-mode-current-line-number-style`. See [configuration.md](configuration.md) See [keybindings.md](keybindings.md) for the full copy mode key reference. ## Format Engine - **140+ tmux-compatible format variables** across sessions, windows, panes, cursor, client, and server - Conditionals (`#{?cond,true,false}`), comparisons (`#{==:a,b}`, `#{!=:a,b}`), boolean logic (`#{||:}`, `#{&&:}`) - Regex substitution (`#{s/pat/rep/:var}`), string manipulation - Loop iteration (`#{W:fmt}`, `#{P:fmt}`, `#{S:fmt}`) over windows, panes, sessions - Truncation, padding, basename, dirname, strftime, shell quoting - Inline style directives: `#[list]`, `#[fill]`, `#[align=left|centre|right]`, `#[range=...]` ## Scripting & Automation - **90+ tmux-compatible commands** : everything you need for automation. Run `psmux list-commands` for the live list - `send-keys`, `capture-pane`, `pipe-pane` for CI/CD and DevOps workflows - `display-popup` for floating popup windows with custom commands - `display-menu` for interactive context menus - `choose-tree` for interactive session/window/pane selection - `choose-buffer` and `choose-client` for interactive buffer and client picking - `if-shell` and `run-shell` for conditional config logic - **30 event hooks** : `after-new-window`, `after-split-window`, `client-attached`, `pane-died`, `pane-focus-in`, `window-linked`, and more. Note that `set-hook` accepts any name without validating it, so a typo silently never fires - **Config file conditionals** : `%if`, `%elif`, `%else`, `%endif` blocks (they nest), plus `%hidden NAME=value`. The condition is a format string, so `%if "#{==:#{@theme},dark}"` works. See [configuration.md](configuration.md) - **User defined command aliases** : `set -g command-alias 'bigsplit=split-window -p 70'` creates your own command name. It resolves from key bindings and config lines. See [configuration.md](configuration.md) - **Positional pane targets** : `select-pane -t '{top-right}'`, plus `{top-left}`, `{bottom-left}`, `{bottom-right}`, `{top}`, `{bottom}`, `{left}`, `{right}`. See [scripting.md](scripting.md) - **Prompt history** : `show-prompt-history` (`showphist`) and `clear-prompt-history` (`clearphist`) inspect and reset what the command prompt remembers. See [scripting.md](scripting.md) - **Session groups** : `set -g session-group ` or `psmux server -g ` links sessions, surfaced by `#{session_group}`, `#{session_group_size}`, and `#{session_grouped}`. See [scripting.md](scripting.md) - Paste buffers, named registers, `display-message` with format variables - Server namespaces via `-L` for running isolated psmux instances - Command chaining with `;` for multi-step bindings - `switch-client` for programmatic session switching - `break-pane` and `join-pane` for pane reorganization - `wait-for` with lock/signal/unlock for cross-pane synchronization - `confirm-before` for user confirmation dialogs See [scripting.md](scripting.md) for full command reference and examples, and [tmux_args_reference.md](tmux_args_reference.md) for per-command flags. ## Session Persistence - psmux session servers survive SSH disconnects and terminal crashes - Detach with `Prefix + d`, reconnect with `psmux attach` from any terminal - Warm sessions (`set -g warm on`, default) pre-spawn background servers for instant session creation - Use [psmux-resurrect](https://github.com/psmux/psmux-plugins/tree/main/psmux-resurrect) to save/restore sessions across reboots - Use [psmux-continuum](https://github.com/psmux/psmux-plugins/tree/main/psmux-continuum) for automatic periodic save/restore ## Session Switching - **Prefix + s** opens an interactive session/window/pane tree chooser - **Prefix + (** and **Prefix + )** cycle through sessions - `switch-client -t sessionname` switches to a named session - `switch-client -l` returns to the last (most recently used) session - Create multiple sessions with `new-session -s name` and switch freely between them ## Display Panes Overlay - **Prefix + q** shows numbered overlays on all panes for quick selection - Press a number key to jump to that pane instantly - Numbers respect `pane-base-index` (e.g., starts from 1 if configured) - Overlay auto-dismisses after `display-panes-time` milliseconds (default: 1000ms) - Only single-digit pane numbers (0 through 9) can be selected by keypress ## Nesting Prevention - psmux automatically detects when running inside an existing session and blocks the launch, so you never end up with a psmux inside a psmux - This matches tmux behavior, where nesting requires explicitly unsetting `$TMUX` - To create a new session from inside psmux, use the command prompt (`Prefix + :`) and type `new-session` - Set `PSMUX_ALLOW_NESTING=1` in the pane's environment when you genuinely want to nest ## Dead Pane Handling - `set -g remain-on-exit on` keeps panes visible after their process exits - Dead panes display their final output for inspection - `respawn-pane` restarts the shell or a new command in a dead pane - Useful for monitoring long-running processes that may crash ## Command Prompt - **Prefix + :** opens a command prompt at the bottom of the screen - Full cursor movement (arrow keys, Home, End) within the command line - Command history (Up/Down arrows recall previous commands) - Any psmux/tmux command can be typed and executed interactively - Supports `source-file`, `set-option`, `split-window`, `list-commands`, and every other psmux command - Prompt history is persistent and inspectable with `show-prompt-history` ## Claude Code Agent Teams - First-class support for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) teammate pane spawning - Automatically sets `TMUX`, `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS`, and teammate mode - Each agent gets its own visible pane with full terminal output - No extra configuration needed: start psmux, run `claude`, and ask it to create a team See [claude-code.md](claude-code.md) for detailed setup and troubleshooting. ## CJK and IME Input - Full support for Chinese, Japanese, and Korean character input - IME composition handled with minimal latency (paste-detection heuristic tuned for rapid IME bursts) - Korean IME input correctly handled without bracketed paste sequence injection - CJK text pasting works reliably for any length - UTF-8 multi-byte characters (box-drawing, emoji, CJK) render correctly in ConPTY panes - Characters outside the Basic Multilingual Plane (emoji, JIS X 0213 kanji such as `𠮟`) arrive whole whether pasted, committed by the IME or picked from the Win+. emoji panel, in panes and in psmux's own prompts, on every host. The console hands such a character over as one key record per UTF-16 half; psmux pairs the halves itself because crossterm 0.29 drops them (#742) ## Interactive Choosers - `choose-tree` (`Prefix + w`): browse and select sessions, windows, and panes interactively, with optional [live preview pane](preview.md) (`p` to toggle, `set -g choose-tree-preview on` to default on) - `choose-session` (`Prefix + s`): browse sessions only, same live preview support - `choose-buffer` (`Prefix + =`): pick from paste buffers with preview - `choose-client`: view connected clients - `customize-mode`: interactive options editor - **Digit-jump** (all pickers): type a number and press `Enter` to jump directly to that row (1-based). A `go to N` indicator appears at the bottom; `Backspace` edits the number, `Esc` cancels. Every row is numbered so the mapping is visible at a glance. See [keybindings.md](keybindings.md#picker-navigation-choose-session-choose-tree-choose-buffer-list-keys-customize) for the full key reference. ## Multi-Shell Support - **PowerShell 7** (default), PowerShell 5, cmd.exe - **Git Bash**, WSL, nushell, and any Windows executable - Sets `TERM=xterm-256color`, `COLORTERM=truecolor` automatically - Sets `TMUX` and `TMUX_PANE` env vars for tmux-aware tool compatibility See [configuration.md](configuration.md) for `default-shell` and other options. ## Named Paste Buffers - `set-buffer -b "text"` to create a named buffer - `show-buffer -b ` to read it back - `paste-buffer -b ` to paste into the active pane - `delete-buffer -b ` to remove it - Named buffers are separate from the anonymous buffer stack - Useful for structured data exchange between automation steps ## Diagnostics - **Debug logging subsystem** : eleven independent loggers, all off by default, each writing a timestamped and capped log covering one subsystem (client rendering, styles, input, server, session registry, mouse, SSH input, latency, popups, warm pool, auth handshake). Turn one on with a single environment variable, for example `$env:PSMUX_INPUT_DEBUG = "1"` - **Always on failure records** : `server-startup.log` captures the real reason a server could not start, including the Windows error and the exact path it tried to spawn, `config-warnings.log` captures config lines that were silently ignored, and `crash.log` captures a panic with a full backtrace - **Predictable state layout** : every runtime file lives under `%USERPROFILE%\.psmux\`, so a bug report can include exactly what is on disk See [diagnostics.md](diagnostics.md) for every log file, what each records, and what to attach to a bug report. ## Developer Integration and tmux API Compatibility psmux is designed as a drop-in replacement for tmux on Windows at the API level: - **Same CLI protocol**: 90+ tmux commands with identical flags, arguments, and output formats. Run `psmux list-commands` for the live list - **Same stable IDs**: `$N` (session), `@N` (window), `%N` (pane) targeting works identically - **Same control mode**: `-C`/`-CC` wire protocol with `%begin`/`%end` framing and async notifications - **Same format engine**: 140+ format variables, conditionals, loops, regex, string ops - **Same config**: Reads `~/.tmux.conf` directly - **libtmux compatible**: The libtmux Python library works with psmux (see note on Windows encoding) - **tmux.exe alias**: psmux installs a `tmux.exe` alias so existing scripts find it on the PATH For a full developer integration guide with examples in Python, PowerShell, Node.js, Go, and Rust, see [integration.md](integration.md). For the tmux command and feature compatibility matrix, see [compatibility.md](compatibility.md). --- # Windows Use Cases Native tmux on Windows is not just "splits and tabs." Because psmux runs a **real background server** that keeps your shells, panes, and running processes alive independently of any window, it unlocks a set of workflows that Windows Terminal tabs, plain PowerShell windows, and Task Manager simply cannot do. This page collects practical, Windows focused scenarios. Every example uses commands that ship with psmux today. Use `psmux`, `pmux`, or `tmux` interchangeably. ## The core idea: a detached server that outlives your window Most of these scenarios rest on one property: a psmux session lives inside a **server process**, not inside your terminal window. Start something detached, close every window, log off, and the process keeps running. Attach later from any terminal to see it live. ```powershell # Start a session in the background (no window opens) psmux new-session -d -s work # ...do other things, close terminals, even sign out... # Later, from any terminal, jump back in exactly where it was psmux attach -t work ``` That single idea (a durable server you attach to and detach from) is what makes the rest of this page possible on Windows. --- ## 1. Run background services at boot, before you log in **The scenario:** You want an OpenVPN tunnel, an SSH reverse tunnel, a syncthing daemon, or a local dev backend to come up automatically when the machine boots, before anyone signs in. Later, after you log in, you want to *see* it (its live log, its status, its prompts) instead of guessing whether it worked. A plain scheduled task can start a process, but it runs invisibly in the background with no console you can inspect. psmux fixes that: start the process inside a **detached psmux session**, then attach to that same session after login to watch it live, read its output, and even interact with it. ### Step 1: Create a startup script `C:\scripts\start-vpn.ps1`: ```powershell # Start (or reuse) a detached psmux session that hosts the VPN psmux has-session -t vpn 2>$null if ($LASTEXITCODE -ne 0) { psmux new-session -d -s vpn -n tunnel -- openvpn --config C:\vpn\client.ovpn } ``` Running the command *inside* a psmux pane (rather than as a bare background process) means its stdout, its status messages, and any interactive prompts are captured in a live terminal you can attach to. ### Step 2: Register it with Task Scheduler Create a task that runs at startup, under **your own user account**, with "Run whether user is logged on or not" checked. Running as your user (not `SYSTEM`) is important: the psmux server writes its discovery files to `~\.psmux\`, so keeping the server in your profile is what lets you attach to it after you log in. ```powershell # Run once, elevated, to register the task $action = New-ScheduledTaskAction -Execute "pwsh.exe" ` -Argument "-NoProfile -WindowStyle Hidden -File C:\scripts\start-vpn.ps1" $trigger = New-ScheduledTaskTrigger -AtStartup $principal = New-ScheduledTaskPrincipal -UserId "$env:USERNAME" -LogonType S4U -RunLevel Highest Register-ScheduledTask -TaskName "psmux-vpn" -Action $action ` -Trigger $trigger -Principal $principal ``` ### Step 3: After login, attach and watch ```powershell # See the tunnel come up, read its log, respond to prompts psmux attach -t vpn # Or just peek at the last 40 lines without attaching psmux capture-pane -p -t vpn:tunnel -S -40 ``` The same pattern works for anything that should be "always on but occasionally watched": a local database, a message broker, a self hosted service, a scheduled backup job, a mining or rendering process, or an SSH tunnel that keeps a port forwarded. > **Tip:** Pair this with `set -g remain-on-exit on` so that if the service crashes, the pane stays open showing its final output instead of vanishing. You can then `respawn-pane -k` to restart it in place. --- ## 2. Long jobs that survive logout, lock, and RDP disconnects **The scenario:** You kick off a multi hour build, a large `robocopy` mirror, a dataset download, a database migration, or a model training run. On a laptop you close the lid; on a server you disconnect your Remote Desktop session. Normally, closing the window (or dropping RDP) can take the process down with it. Run it inside psmux and it keeps going. RDP disconnects, sign outs, and closed terminals do not touch the psmux server. ```powershell # Start the long job detached psmux new-session -d -s build -- pwsh -c "cargo build --release; Write-Host DONE" # Disconnect RDP, go home, come back tomorrow... # Reattach and check on it psmux attach -t build ``` This is the classic reason Linux admins live in tmux, and it applies just as much to Windows servers you reach over RDP or SSH. The job is anchored to the server, not to your session. > **Note:** psmux sessions survive RDP disconnects and SSH drops, but a full machine **reboot** stops the server. To restore your window and pane layout across reboots, use the [psmux-resurrect and psmux-continuum plugins](plugins.md). --- ## 3. A one command operations dashboard **The scenario:** You want a single command that lays out a live monitoring cockpit: CPU and memory on top, network below, and a tail of a log file beside them. On Windows this normally means juggling several separate windows. A short script builds the whole layout at once. This pairs beautifully with the [related psmux tools](../README.md#related-projects) `pstop` (a Windows htop) and `psnet` (a live network monitor). `C:\scripts\dashboard.ps1`: ```powershell psmux new-session -d -s dash -n monitor -- pstop psmux split-window -t dash:monitor -h -- psnet psmux split-window -t dash:monitor -v -c "C:\logs" -- pwsh -c "Get-Content app.log -Wait -Tail 20" psmux select-layout -t dash:monitor tiled psmux attach -t dash ``` Now `pwsh C:\scripts\dashboard.ps1` gives you an instant, reproducible ops view. Detach with `Prefix + d` and the monitors keep running in the background; reattach any time. You can drive it entirely from scripts too: ```powershell # Add a fourth pane later without touching the mouse psmux split-window -t dash:monitor -v -- pwsh -c "Get-Content C:\logs\error.log -Wait" # Broadcast the same command to every pane (great for fleets of shells) psmux setw synchronize-panes on psmux send-keys "cd C:\project" Enter psmux setw synchronize-panes off ``` --- ## 4. Administer Windows servers over SSH with persistent sessions **The scenario:** You SSH into a Windows box (Windows now ships an OpenSSH server) to run maintenance. Halfway through a long operation your connection drops. Without a multiplexer, your work dies with the connection. Install psmux on the server, and every remote session becomes durable: ```powershell # On the remote Windows host, over SSH: psmux new-session -A -s admin # attach if it exists, otherwise create # Connection drops? Reconnect and run the same command to land back where you were. psmux new-session -A -s admin ``` The `-A` flag means "attach or create," so a single command is safe whether or not the session already exists. Combined with psmux's [full mouse over SSH support](mouse-ssh.md), you get a real multiplexer experience on remote Windows servers, the same way tmux serves Linux admins. --- ## 5. Run and watch AI coding agents in parallel This is where native tmux on Windows shines for modern workflows. AI CLI agents (Claude Code, the `pi` agent, opencode, aider, Codex CLI, Gemini CLI, and others) are long running, interactive terminal programs. psmux is purpose built to host them: give each agent its own visible pane, run several at once, and script their input and output. ### First class Claude Code agent teams psmux has dedicated support for Claude Code. When Claude runs inside a psmux session, its teammate agents spawn into **separate visible panes** instead of hiding in process, so you can watch every agent work. ```powershell psmux new-session -s work claude # ask Claude to create a team; panes appear per teammate ``` No configuration required. Full details, including the two agent systems and how to steer Opus toward visible panes, are in the [Claude Code guide](claude-code.md). ### Run several different agents side by side Because each agent is just a program in a pane, you can run a whole bench of them at once and compare: ```powershell psmux new-session -d -s agents -n claude -- claude psmux new-window -t agents -n opencode -- opencode psmux new-window -t agents -n aider -- aider psmux new-window -t agents -n pi -- pi psmux attach -t agents ``` Cycle through them with `Prefix + n` / `Prefix + p`, or split them into one tiled window to watch them simultaneously. Each keeps its own scrollback and state. ### Script and supervise agents programmatically psmux gives you a scripting handle on any agent's terminal. You can feed input with `send-keys` and read what the agent produced with `capture-pane`, which turns a psmux server into a lightweight harness for driving interactive agents from PowerShell: ```powershell # Send a prompt into a running agent pane psmux send-keys -t agents:aider "refactor the auth module" Enter # Read back what it printed (the last 200 lines) psmux capture-pane -p -t agents:aider -S -200 # Log everything an agent does to a file for later review psmux pipe-pane -t agents:claude -o "pwsh -c 'Add-Content C:\logs\claude.log'" ``` This is the same capability that makes psmux the substrate for tools like the Claude Code teammate system: a durable, scriptable place to run interactive processes and observe them. If you build your own automation on top of Windows tmux, [libtmux and control mode](integration.md) give you Python and IDE grade hooks into the same server. > **Tip:** Keep an agent alive but detached so it can churn on a long task while you do other work. Start it with `new-session -d`, then attach only when you want to check in or answer a prompt. --- ## 6. Reproducible development environments in one command **The scenario:** Every time you start work on a project you open the editor, a build watcher, a dev server, and a log tail. That is four windows to arrange by hand. Script the whole project layout once and launch it with a single command: `C:\scripts\dev-myapp.ps1`: ```powershell $proj = "C:\Projects\myapp" psmux new-session -d -s myapp -n edit -c $proj -- nvim psmux new-window -t myapp -n server -c $proj -- pwsh -c "npm run dev" psmux new-window -t myapp -n build -c $proj -- pwsh -c "cargo watch -x build" psmux split-window -t myapp:build -v -c $proj -- pwsh -c "Get-Content .\dev.log -Wait" psmux select-window -t myapp:edit psmux attach -t myapp ``` `pwsh C:\scripts\dev-myapp.ps1` now boots your entire workspace, in the right directories, running the right commands, in seconds. Detach when you step away; the dev server and watcher keep running. This is the Windows equivalent of the tmuxinator / tmuxp project workflows Linux developers rely on. --- ## 7. Drive interactive Windows programs from scripts **The scenario:** You need to automate a program that expects an interactive console (a REPL, an installer prompt, a database shell, an SSH session that asks a question). Plain redirection does not work with programs that talk to the console directly. Because psmux hosts each program in a real ConPTY, you can script it as if a human were typing. ```powershell # Start a Python REPL detached psmux new-session -d -s repl -- python # Feed it statements psmux send-keys -t repl "import platform; print(platform.system())" Enter Start-Sleep -Milliseconds 200 # Read the answer back psmux capture-pane -p -t repl -S -5 # Clean up psmux kill-session -t repl ``` This pattern (send input, wait, capture output) lets you build unattended flows around tools that were never designed to be scripted, all on native Windows without WSL. The same `send-keys` plus `capture-pane` loop is how psmux's own test suite drives real interactive programs. --- ## 8. Capture and log everything a scheduled job does **The scenario:** A nightly job runs unattended and you want a durable, timestamped record of exactly what happened on screen, not just its exit code. `pipe-pane` mirrors a pane's output to a file or command in real time: ```powershell # Start the job in a detached pane psmux new-session -d -s nightly -- pwsh -c "C:\scripts\backup.ps1" # Mirror everything the pane prints to a dated log file psmux pipe-pane -t nightly -o "pwsh -c 'Add-Content C:\logs\backup-$(Get-Date -Format yyyyMMdd).log'" ``` Combined with hooks, you can react to events automatically. For example, flash the status bar or fire a notification when a monitored pane goes silent or a process dies: ```powershell psmux set-hook -g pane-died "display-message 'A job pane exited'" psmux set-hook -g alert-silence "run-shell -b 'C:\scripts\notify.ps1'" ``` See the [Scripting guide](scripting.md) for the full hook catalog, format variables, and every command referenced on this page. --- ## Why this needs a multiplexer (and not just tabs) | Capability | Windows Terminal tabs | psmux | |------------|:---------------------:|:-----:| | Process survives closing the window | No | Yes | | Process survives logout / RDP disconnect | No | Yes | | Start a job before login, watch it after | No | Yes | | Attach and detach from a running session | No | Yes | | Script pane input and read pane output | No | Yes | | One command rebuilds a whole layout | No | Yes | | Broadcast one command to many shells | No | Yes | | Live output logging with `pipe-pane` | No | Yes | | Event hooks (pane died, silence, activity) | No | Yes | Windows Terminal draws windows. psmux runs a **server**. That difference is the whole point, and it is what brings the decades of tmux driven Linux and macOS workflows to native Windows. ## Related reading | Topic | Description | |-------|-------------| | [Getting Started on Windows](tutorials/getting-started-windows.md) | First session, prefix key, splits, detach and reattach, first config file | | [Cross Platform tmux Scripts](tutorials/cross-platform-tmux-scripts.md) | One bootstrap script that runs on Windows through psmux and on Linux or macOS through tmux | | [Terminal AI Agents and TUIs](tutorials/terminal-agents-and-tuis.md) | Scenario 5 in depth: driving, logging and restarting agents and TUIs from scripts | | [Dev Environment Layouts](tutorials/dev-environment-layouts.md) | Scenario 6 in depth: layouts, sizing, synchronize-panes, save and restore | | [Architecture](architecture.md) | Why the detached server model works natively on Windows: ConPTY per pane, client and server, warm pool | | [Scripting & Automation](scripting.md) | Every command used above, plus hooks, targets, and format variables | | [Claude Code Agent Teams](claude-code.md) | Deep dive on running AI agents in visible panes | | [Developer Integration](integration.md) | Python (libtmux), control mode, and IDE integration | | [Warm Sessions](warm-sessions.md) | How psmux makes session and pane creation near instant | | [Mouse Over SSH](mouse-ssh.md) | Full mouse support on remote Windows servers | | [Plugins & Themes](plugins.md) | psmux-resurrect and psmux-continuum for save/restore across reboots | --- # Write One tmux Script That Runs on Windows, Linux and macOS with psmux psmux is a tmux compatible terminal multiplexer for Windows, and it ships a `tmux.exe` alias. That means automation written against tmux, whether a session bootstrap script, a project sessionizer, a CI harness that drives a TUI, or an agent orchestrator built on `send-keys` and `capture-pane`, runs on Windows unchanged. This tutorial shows the same script running under Git Bash on Windows through psmux, the PowerShell equivalent for pure Windows shops, the quoting and `--` rules psmux applies, the exit codes and error strings you can script against, and the short list of platform differences a cross platform script needs to know about. Every command and every output block on this page was run against psmux 3.3.8 on Windows 11. **What you will learn** - Why `tmux` on a Windows PATH can be psmux, and what `tmux -V` reports - A session bootstrap script in bash that runs on all three operating systems, and its PowerShell twin - The idempotent `has-session` pattern and what exit codes to expect - How psmux parses `--`, dash leading operands, quotes, backslashes and newlines - `display-message -p`, `-F` formats, `wait-for`, `pipe-pane` and `run-shell` from scripts - A portability checklist and a headless CI style test that asserts on `capture-pane` output ## Why does a tmux script run on Windows at all? psmux installs three binaries: `psmux.exe`, `pmux.exe` and `tmux.exe`. They are the same program. The command names, flags, target syntax (`session:window.pane`, `%pane_id`, `@window_id`), format variables and config syntax are tmux's, and the version string is tmux shaped so version checks keep passing: ```console $ tmux -V tmux 3.3.8 $ psmux -V psmux 3.3.8 (cbb9c10 2026-08-28) ``` So a script that calls `tmux` finds psmux on Windows and real tmux on Linux and macOS. You write it once. The [Compatibility](../compatibility.md) page lists every command and option; `psmux list-commands` prints the live list for the build you have. ## A session bootstrap script in bash (Git Bash on Windows, sh on Linux and macOS) `dev.sh` creates a project session with an editor window in the `main-vertical` layout and a background logs window, sends a command into two panes, and prints what it built. Run it twice and it is a no op the second time. ```bash #!/usr/bin/env bash # dev.sh: bootstrap a project session. Runs unchanged on Linux/macOS (tmux) and Windows (psmux). set -e SESSION="${1:-dev}" if tmux has-session -t "$SESSION" 2>/dev/null; then echo "session $SESSION already exists" else tmux new-session -d -s "$SESSION" -n editor tmux split-window -h -t "$SESSION:editor" tmux split-window -v -t "$SESSION:editor.1" tmux select-layout -t "$SESSION:editor" main-vertical tmux new-window -d -t "$SESSION" -n logs tmux send-keys -t "$SESSION:editor.0" 'echo hello from pane 0' Enter tmux send-keys -t "$SESSION:logs" 'echo log window ready' Enter fi tmux list-windows -t "$SESSION" -F '#{window_index}:#{window_name} panes=#{window_panes}' tmux display-message -p -t "$SESSION:editor.0" 'active pane #{pane_id} in #{session_name}:#{window_name} (#{pane_width}x#{pane_height})' ``` Run from Git Bash on Windows (`C:\Program Files\Git\bin\bash.exe`), with psmux answering as `tmux`: ```console $ bash ./dev.sh tutaDev 0:editor panes=3 1:logs panes=1 active pane %1 in tutaDev:editor (71x30) $ bash ./dev.sh tutaDev session tutaDev already exists 0:editor panes=3 1:logs panes=1 active pane %1 in tutaDev:editor (71x30) ``` The panes are running PowerShell 7 (the psmux default shell), and `capture-pane` shows the command landed: ```console $ tmux capture-pane -p -t tutaDev:editor.0 PS C:\Users\godwin\projects\api> echo hello from pane 0 hello from pane 0 ``` (`echo` in PowerShell is `Write-Output` and prints each argument on its own line. That is the shell, not psmux; quote the string or use `Write-Host` if you want one line.) Attach with `tmux attach -t tutaDev` from any terminal, and `tmux kill-session -t tutaDev` when done. ## The same bootstrap in PowerShell If your team is Windows only, write the script in PowerShell. The psmux commands are identical; only the surrounding shell changes. Note `$LASTEXITCODE` for the `has-session` check, and `${Session}:editor` so PowerShell does not read `$Session:` as a scoped variable. ```powershell # dev.ps1: the same session bootstrap, written for PowerShell. param([string]$Session = "dev") psmux has-session -t $Session 2>$null if ($LASTEXITCODE -ne 0) { psmux new-session -d -s $Session -n editor psmux split-window -h -t "${Session}:editor" psmux split-window -v -t "${Session}:editor.1" psmux select-layout -t "${Session}:editor" main-vertical psmux new-window -d -t $Session -n logs psmux send-keys -t "${Session}:editor.0" 'Write-Host "hello from pane 0"' Enter psmux send-keys -t "${Session}:logs" 'Write-Host "log window ready"' Enter } else { Write-Host "session $Session already exists" } psmux list-windows -t $Session -F '#{window_index}:#{window_name} panes=#{window_panes}' psmux display-message -p -t "${Session}:editor.0" 'active pane #{pane_id} in #{session_name}:#{window_name} (#{pane_width}x#{pane_height})' ``` ```console PS> pwsh -NoProfile -File .\dev.ps1 tutaDevPs 0:editor panes=3 1:logs panes=1 active pane %1 in tutaDevPs:editor (71x30) PS> pwsh -NoProfile -File .\dev.ps1 tutaDevPs session tutaDevPs already exists 0:editor panes=3 1:logs panes=1 active pane %1 in tutaDevPs:editor (71x30) ``` ## How do I make a script idempotent with has-session? `has-session -t NAME` exits 0 when the session exists and 1 when it does not, in tmux and in psmux. Always redirect stderr, because tmux prints `can't find session` on the failure path. ```bash if ! tmux has-session -t "$S" 2>/dev/null; then tmux new-session -d -s "$S" fi ``` `new-session -A -s NAME` is the one line version for the interactive case (attach if it exists, create otherwise), and `new-session -A -d -s NAME` does the same without attaching. Both work in psmux. ## What exit codes and error strings can I script against? An unresolvable target is an error with a non zero exit, never a silent fallback to the active window (this was tightened in psmux 3.3.8, issue #545): ```console $ tmux send-keys -t tutaProbe:nope 'x' Enter; echo rc=$? psmux: can't find window: nope rc=1 $ tmux capture-pane -p -t %999; echo rc=$? psmux: can't find pane: %999 rc=1 $ tmux has-session -t tutaMissing; echo rc=$? rc=1 ``` The message prefix is `psmux:` rather than `tmux:`. If you match on the text, match on the part after the colon (`can't find window`, `can't find pane`, `can't find session`), which is tmux's wording. `kill-session -t NAME` on a name that is not a session exits 1 with `can't find session: NAME`, as in tmux, so a typo in a teardown script is not a silent success. `list-sessions` with nothing running prints `no server running on ` and exits 1, as tmux does, which keeps the `tmux ls 2>/dev/null || tmux new -d -s work` idiom portable. A `-f` filter that matches nothing on a live server is still an empty listing at exit 0. ## How does psmux parse `--`, dashes, quotes and backslashes? This is where scripts most often differ between shells, so here are the exact rules psmux applies. All of them match tmux unless noted. **`--` ends option parsing.** A dash leading operand after `--` is data. `send-keys -- -la Enter` types `-la` into the pane, and `set-option -- @k -u` stores the string `-u` in `@k` instead of unsetting it: ```console $ tmux send-keys -t tutaProbe -- -la Enter $ tmux set-option -t tutaProbe -- @marker -dash-value $ tmux show-options -t tutaProbe | grep marker @marker "-dash-value" ``` Put the `--` before the option name for `set-option` and `set-hook`, exactly as you would with tmux. **`send-keys -l` is literal.** Nothing after `-l` is parsed as a key name, so `Enter`, `C-c` and friends are typed as text. Send the key on a separate call: ```bash tmux send-keys -t "$S" -l 'literal $env:TEMP "quoted" a\b' tmux send-keys -t "$S" Enter ``` Both the `$`, the double quotes and the backslash arrive intact (psmux escapes `"` and `\` on the wire and unescapes them in the server, issue #547). **Newlines in a payload are safe.** A `send-keys` argument that contains a newline is delivered as one argument and cannot split into a second psmux command (issue #560). It is typed as text rather than as a key press, so send multi line input as separate `send-keys` calls with `Enter` between them. **Multiple tokens after `--` are executed directly; a single string goes through the shell.** `new-window -- ping -n 30 127.0.0.1` starts `ping` as the pane's process, with no shell in between, the way tmux calls `execvp`. A single quoted string is handed to the pane's shell so pipes and redirects work: ```console $ tmux new-window -d -t tutaR -n direct -- ping -n 30 127.0.0.1 $ tmux display-message -p -t tutaR:direct '#{window_name} runs #{pane_current_command}' direct runs PING $ tmux new-window -d -t tutaR -n shstr "ping -n 30 127.0.0.1 | findstr Reply" $ tmux display-message -p -t tutaR:shstr '#{window_name} runs #{pane_current_command}' shstr runs findstr ``` **Shell quoting is the caller's job, and it differs by shell.** In bash and in PowerShell, single quotes pass the text to psmux untouched, so `'echo "pid=$PID"'` reaches the pane with the `$` intact and the pane's shell expands it. In PowerShell double quotes, `$env:TEMP` and `$PID` are expanded by the calling shell before psmux ever sees them. In bash, the same is true of `"$HOME"`. Use single quotes around anything the pane should expand. ```console $ tmux send-keys -t tutaQ 'echo "pid=$PID home=$HOME"' Enter $ tmux capture-pane -p -t tutaQ | grep pid= pid=27140 home=C:\Users\godwin ``` Chaining several commands in one invocation uses `\;` in bash and `` `; `` in PowerShell, because each shell treats a bare `;` as its own separator. See [Scripting](../scripting.md) for more. ## How do I read state back: display-message -p and -F formats `display-message -p` prints a format string and exits, which is the standard way to pull one value into a script. `-F` on the `list-*` commands does the same for lists. psmux supports 140+ tmux format variables and the `#{==:a,b}`, `#{?cond,yes,no}` and modifier forms. ```bash pane=$(tmux display-message -p -t "$S:editor.0" '#{pane_id}') tmux list-panes -s -t "$S" -F '#{window_index}.#{pane_index} #{pane_current_command} #{pane_width}x#{pane_height}' tmux if-shell -F '#{==:#{session_name},tutaR}' 'display-message -p format-yes' 'display-message -p format-no' ``` `#{pane_current_path}` returns a Windows path on Windows (`C:\Users\godwin\projects\api`). In Git Bash, convert it when you need a POSIX path: ```console $ cygpath -u "$(tmux display-message -p -t tutaR '#{pane_current_path}')" /c/Users/godwin/Documents/workspace/psmux ``` ## wait-for, pipe-pane and run-shell from a script **`wait-for`** blocks until another process signals the same channel. It is the tmux way to sequence steps across panes, and it works the same in psmux: ```console $ ( sleep 2; tmux wait-for -S tutaGo ) & $ tmux wait-for tutaGo; echo "released rc=$?" released rc=0 ``` **`pipe-pane`** streams a pane's raw output to a sink. The sink runs on the Windows side, so give it a Windows path. In Git Bash `$TEMP` and `$TMP` are mapped to `/tmp`, which the psmux server cannot open; use `cygpath -w` or a literal `C:/...` path: ```bash LOG="$(cygpath -w "$TEMP")\\pane.log" tmux pipe-pane -t "$S" "cat >> '$LOG'" tmux send-keys -t "$S" 'echo piped-line-1' Enter tmux pipe-pane -t "$S" # no argument turns the pipe off ``` ```console $ grep -c piped-line-1 "C:/Users/godwin/AppData/Local/Temp/pane.log" 2 ``` (Two hits: the echoed command line and its output.) `cat >> file` sinks are serviced by the psmux server itself so they need no `cat.exe` on PATH; other sinks run through the shell. psmux does not expand `#{...}` in the sink command, where tmux does; expand it in the caller if you need `#{pane_id}` in the file name. **`run-shell` and `if-shell` use the platform's shell.** On Linux and macOS tmux runs them with `sh -c`. On Windows psmux runs them with `pwsh -NoProfile -Command` (falling back to Windows PowerShell, then `cmd /c`). This is the one place where a tmux script is not automatically portable: `run-shell 'test -f ~/.x && ...'` is sh syntax and will not run under pwsh. Keep those commands to things both shells accept, or branch. A cheap branch that works in both places, because `Get-Command` does not exist in sh and so fails there: ```console $ tmux if-shell 'Get-Command pwsh' 'display-message -p yes-pwsh' 'display-message -p no-pwsh' yes-pwsh ``` In a config file: ```tmux if-shell 'Get-Command pwsh' 'source-file ~/.tmux.windows.conf' 'source-file ~/.tmux.unix.conf' ``` Format variables are expanded in `run-shell` arguments before the command runs, on both sides: ```console $ tmux run-shell -t tutaProbe 'echo run-shell says #{session_name}:#{pane_id}' run-shell says tutaProbe:%1 ``` (One word per line is PowerShell's `echo` again. Both the format variables were expanded before the command ran, which is the point.) ## What is set inside a pane: TMUX and TMUX_PANE Processes inside a psmux pane see `TMUX` and `TMUX_PANE`, so the common guard `[ -n "$TMUX" ]` and tools that read `$TMUX_PANE` work: ```console $ tmux send-keys -t tutaProbe 'echo "TMUX=$env:TMUX PANE=$env:TMUX_PANE"' Enter $ tmux capture-pane -p -t tutaProbe | grep TMUX= TMUX=/tmp/psmux-53400/default,61721,0 PANE=%1 ``` The `TMUX` value has tmux's `socket,pid,index` shape, but the first field is not a Unix socket path on Windows; psmux clients and servers talk over a loopback TCP port. Do not parse it for a socket to connect to. Use the CLI, or [control mode](../control-mode.md) (`psmux -C`) for a machine readable stream. Inside the pane, `$TMUX_PANE` is the current pane id (`%1`), which is what `tmux send-keys -t "$TMUX_PANE"` style self targeting scripts expect. ## Portability checklist | Concern | tmux on Linux/macOS | psmux on Windows | What to do in the script | |---------|--------------------|------------------|--------------------------| | Binary name | `tmux` | `tmux`, `psmux` or `pmux` | Call `tmux` | | Version check | `tmux 3.x` | `tmux 3.3.8` | Parse `tmux -V` as usual | | Default shell in panes | `$SHELL` | `SHELL` when it names a usable Windows executable, else `pwsh` (then `powershell`, then `cmd`) | Set `default-shell` or send shell neutral commands | | `run-shell` / `if-shell` interpreter | `sh -c` | `pwsh -NoProfile -Command` | Keep commands shell neutral, or branch with `if-shell 'Get-Command pwsh'` | | Paths from `#{pane_current_path}` | POSIX | Windows (`C:\...`) | `cygpath -u` in Git Bash when needed | | `pipe-pane` sink path | POSIX | Windows path | `cygpath -w "$TEMP"` in Git Bash | | `#{pane_current_path}` inside WSL | n/a | Frozen unless the Linux shell emits OSC 7 | Add the one line from the [FAQ](../faq.md) to `~/.bashrc` in the distro | | Unresolvable `-t` target | error, rc 1 | error, rc 1 (`psmux: can't find ...`) | Match the text after `psmux:`/`tmux:` | | `kill-session` on a missing name | `can't find session`, rc 1 | `can't find session`, rc 1 | Same code | | `list-sessions` with no server | `no server running on `, rc 1 | `no server running on `, rc 1 | `tmux ls 2>/dev/null \|\| tmux new -d` works on both | | `$TMUX` inside a pane | socket path | not a socket | Use only as a "running inside tmux" flag | | `--` end of options | supported | supported | Use it before any dash leading operand | | `send-keys -l`, newline payloads | supported | supported | Same code | | `wait-for`, `capture-pane -p`, `display-message -p`, `-F` | supported | supported | Same code | | Control mode `-C` / `-CC` | supported | supported | Same protocol, see [Control Mode](../control-mode.md) | | Quoting | bash rules | bash rules under Git Bash, PowerShell rules under pwsh | Single quote anything the pane should expand | ## Worked example: a project sessionizer A sessionizer takes a folder, derives a session name from it, builds the layout if needed and attaches. This one runs under bash on all three platforms; it uses `cygpath` only when it exists, so the same file works on Linux. ```bash #!/usr/bin/env bash # sessionize.sh set -e dir="${1:-.}" if command -v cygpath >/dev/null 2>&1; then dir="$(cygpath -w "$dir")"; fi name="$(basename "$dir" | tr '. ' '__')" if ! tmux has-session -t "$name" 2>/dev/null; then tmux new-session -d -s "$name" -c "$dir" -n code tmux split-window -h -t "$name:code" -c "$dir" tmux split-window -v -t "$name:code.1" -c "$dir" tmux select-layout -t "$name:code" main-vertical tmux new-window -d -t "$name" -n git -c "$dir" tmux send-keys -t "$name:git" 'git status' Enter tmux select-window -t "$name:code" fi if [ -n "$TMUX" ]; then tmux switch-client -t "$name" else tmux attach -t "$name" fi ``` Bind it to a key so `Prefix + f` builds and switches to a project, the same binding on every machine: ```tmux bind-key f run-shell -b "bash ~/bin/sessionize.sh ~/projects/api" ``` The PowerShell version differs only in syntax; the psmux calls are the same. Layout recipes for other kinds of projects are in [Dev environment layouts](dev-environment-layouts.md). ## Worked example: a CI style headless TUI test Multiplexers make full screen programs testable without a display. Start the program in a detached session of a known size, type into it, and assert on `capture-pane`. This script launches Neovim, inserts a line, turns on line numbers, and checks the rendered screen. It passed on Windows through psmux and needs no change for Linux: ```bash #!/usr/bin/env bash # ci-check.sh: drive a full screen TUI headlessly and assert on what it drew. set -e S=ci-nvim tmux kill-session -t $S 2>/dev/null || true tmux new-session -d -s $S -x 100 -y 30 -n tui tmux send-keys -t $S:tui 'nvim --clean' Enter # wait until the editor has painted its welcome screen for i in $(seq 1 30); do if tmux capture-pane -p -t $S:tui | grep -q 'NVIM'; then break; fi sleep 0.5 done tmux send-keys -t $S:tui 'i' 'hello from a headless tui' Escape tmux send-keys -t $S:tui ':set nu' Enter sleep 1 if tmux capture-pane -p -t $S:tui | grep -Eq '1 +hello from a headless tui'; then echo "PASS: the editor shows the typed line with a line number" rc=0 else echo "FAIL: unexpected screen" tmux capture-pane -p -t $S:tui rc=1 fi tmux send-keys -t $S:tui ':q!' Enter tmux kill-session -t $S exit $rc ``` ```console $ bash ./ci-check.sh PASS: the editor shows the typed line with a line number ``` Two habits make these tests reliable on any platform: poll `capture-pane` for a marker instead of sleeping a fixed time, and fix the pane size with `-x`/`-y` so line wrapping is the same everywhere. In GitHub Actions the same script runs on both runners with `shell: bash`; install tmux with `apt` on Ubuntu and psmux with `choco install psmux` (or `winget`, `scoop`, `cargo install psmux`) on Windows, and the job body does not change. ## Where to go next - [Scripting and Automation](../scripting.md): every command with examples, hooks, targets, popups, pipe-pane - [Command Reference](../tmux_args_reference.md): per command flag tables - [Compatibility](../compatibility.md): the tmux command and option matrix - [Configuration](../configuration.md): config files, options, environment variables - [Control Mode](../control-mode.md): the `-C` protocol for IDEs and tools - [Multi-Shell](../multi-shell.md): pwsh, cmd, Git Bash, WSL and nushell in panes - [Terminal agents and TUIs](terminal-agents-and-tuis.md) and [Windows use cases](../use-cases.md) ## FAQ **Q: Does psmux understand tmux's target syntax (`session:window.pane`, `%id`, `@id`)?** A: Yes. `dev:editor.0`, `dev:1`, `%3` and `@1` style targets resolve the way tmux resolves them, and an invalid one errors with a non zero exit code. **Q: Can I use my tmux sessionizer script from Linux on Windows?** A: If it is bash, run it under Git Bash and it will call psmux's `tmux.exe`. Convert paths with `cygpath -w` before passing them to `-c`, and check any `run-shell` or `if-shell` bodies, which run under PowerShell on Windows. **Q: Why does `echo hello world` in a pane print one word per line?** A: The pane is running PowerShell, whose `echo` is `Write-Output` and prints each argument separately. Quote the string, use `Write-Host`, or set `default-shell` to the shell you expect. **Q: Do format variables like `#{pane_current_command}` and `#{pane_current_path}` work on Windows?** A: Yes. `pane_current_command` reports the foreground process (`findstr`, `PING`, `nvim`), and `pane_current_path` follows `cd` in PowerShell, cmd, Git Bash and Cygwin. Inside WSL it needs the shell to announce its directory with OSC 7; the [FAQ](../faq.md) has the line. **Q: Is there a machine readable interface instead of parsing text?** A: Yes, control mode. `psmux -C` gives the same `%begin`/`%end`/`%output` stream as `tmux -C`, so tools built on it work unchanged. See [Control Mode](../control-mode.md). **Q: Will `send-keys` deliver Ctrl keys and special keys the same way?** A: Yes: `Enter`, `Tab`, `Escape`, `C-c`, `M-x`, `F1` to `F12`, arrows, `PageUp`, `Home`, `End` and the rest of tmux's key names are accepted, and `-l` types the text literally. --- # Running Terminal AI Agents and Multi Pane TUIs on Windows with psmux This tutorial shows how to run terminal AI coding agents (Claude Code, Codex CLI, Gemini CLI, aider, opencode and similar) and full screen TUI programs (neovim, lazygit, yazi, btop, k9s, gitui, htop inside WSL) inside psmux, the native tmux for Windows. It is written for Windows developers who want several agents or tools running side by side in one terminal, driven and observed from PowerShell scripts, with sessions that keep running after the window is closed. Every command below was run against psmux 3.3.8 on Windows 11 with PowerShell 7 and Windows Terminal. `psmux`, `pmux` and `tmux` are the same binary, so any example works with `tmux` in front of it too. ## What you will learn - Why a native terminal multiplexer matters for agents and TUIs on Windows (each pane is a real ConPTY, each agent is a real Windows process) - A "watch an agent work" layout: agent in a large pane, its log in a side pane, a shell underneath - A multi agent grid: several agents in parallel windows, driven with `send-keys`, read with `capture-pane`, logged with `pipe-pane` - Detecting when an agent finishes, restarting a crashed agent with `respawn-pane`, and keeping dead panes visible with `remain-on-exit` - Naming panes per agent with `select-pane -T` and showing the active agent in the status line - Running neovim, yazi and lazygit in a pane on PowerShell and inside WSL, and what the mouse wheel does in each Related pages: [Claude Code agent teams](../claude-code.md) for the Claude Code specific setup, [Scripting and Automation](../scripting.md) for the full command reference, [Windows Use Cases](../use-cases.md) for more scenarios, and [Multi Window and Multi Pane Development Layouts](dev-environment-layouts.md) for layout design. ## Why run terminal agents inside a multiplexer on Windows? A terminal agent is just a long running console program that reads a prompt, prints a lot, and sometimes waits for you. Windows Terminal tabs can hold several of them, but tabs are not scriptable, do not survive the window closing, and cannot be read back by a script. psmux gives you, on Windows, the same properties tmux gives on Linux and macOS: | Property | What it means for agents and TUIs | |---|---| | **Each pane is a real ConPTY** | The agent sees a genuine console with a size, colours, cursor and key input. TUIs render exactly as they would in a plain Windows Terminal tab. | | **Each agent is a real Windows process** | `#{pane_pid}` and `#{pane_current_command}` tell you what is running. Task Manager sees it. Nothing is emulated. | | **Sessions live in a background server** | Close Windows Terminal, sign out of RDP, reconnect tomorrow: `psmux attach` shows the agent exactly where it was. | | **Everything is a tmux command** | `send-keys`, `capture-pane`, `pipe-pane`, `respawn-pane`, `list-panes -F` all work from PowerShell, cmd, bash or another agent. The same script drives tmux on Linux. | | **Native multiplexing** | psmux is a single Rust binary talking to ConPTY directly. No WSL, no Cygwin, no MSYS layer between the agent and the console. | If you have used tmux to babysit agents on a Linux box, everything on this page will look familiar. If you have not, the patterns are simple: put each agent in its own pane or window, give it a name, log it, and poll it. ## Layout 1: watch one agent work The most common setup. The agent gets most of the screen, a narrow pane on the right tails its log, and a small shell underneath is where you run git or tests without interrupting the agent. ``` +-------------------------------------------+---------------+ | | | | agent: claude (pane 0, 70%) | logs (pane 2)| | | | | | | +-------------------------------------------+ | | shell (pane 1, 25%) | | +-------------------------------------------+---------------+ ``` ```powershell $s = 'work' psmux new-session -d -s $s -n agent -x 160 -y 45 # Right column for logs, then a bottom strip under the agent for a shell psmux split-window -t "${s}:agent" -h -l 30% -d psmux split-window -t "${s}:agent.0" -v -l 25% -d # Name the panes. -T only sets the title, it never changes which pane is active. psmux select-pane -t "${s}:agent.0" -T 'agent: claude' psmux select-pane -t "${s}:agent.1" -T 'shell' psmux select-pane -t "${s}:agent.2" -T 'logs' # Log the agent pane to a file, then tail it in the logs pane psmux pipe-pane -t "${s}:agent.0" -o "cat > $env:TEMP\claude.log" psmux send-keys -t "${s}:agent.2" "Get-Content -Wait $env:TEMP\claude.log" Enter # Start the agent (replace with codex, gemini, aider, opencode ...) psmux send-keys -t "${s}:agent.0" 'claude' Enter psmux attach -t $s ``` What the server reports after the splits and titles: ``` PS> psmux list-panes -t work:agent -F '#{pane_index} #{pane_id} #{pane_width}x#{pane_height} title=#{pane_title} active=#{pane_active}' 0 %1 111x33 title=agent: claude active=1 1 %3 111x11 title=shell active=0 2 %2 48x45 title=logs active=0 ``` Pane 0 stayed active while panes 1 and 2 were titled. That is the tmux behaviour restored in [#592](https://github.com/psmux/psmux/issues/592): `select-pane -T` and `-P` are title and style only. To show the titles on the pane borders, add this to `~/.psmux.conf` (or run it with `psmux set -g ...`): ```tmux set -g pane-border-status top set -g pane-border-format " #{pane_index}: #{pane_title} [#{pane_current_command}] " ``` ### Why `pipe-pane` with `cat >` works on Windows `cat` is an alias for `Get-Content` in PowerShell and never reads stdin, so the tmux logging idiom could not work as a shell command on Windows. Since [#576](https://github.com/psmux/psmux/issues/576) the psmux server recognises `cat > ` and `cat >> ` and writes the pane's raw ConPTY bytes to that file itself, no shell involved. Use an absolute path. The file contains escape sequences (colours, cursor moves) exactly as the agent emitted them, which is what you want for a faithful transcript. Details and the PowerShell sink alternative are in [Piping Pane Output](../scripting.md#piping-pane-output-pipe-pane). Checked live: after `Write-Host "logged line two"` in the piped pane, the file held 127 bytes including the prompt redraw and the text, and `pipe-pane` with no `-o` stopped the pipe with exit code 0. ## Layout 2: a grid of agents driven from a script When you want several agents working on different parts of a project at the same time, give each one a window. Windows are cheap, every one has a name, and a script can address them by name. ``` status line: [work] 0:hub 1:api* 2:web 3:tests +------------------------------------------------------------+ | | | window "api": agent working on the API | | (switch with prefix + 2, or psmux select-window -t api) | | | +------------------------------------------------------------+ ``` The full script. It uses a stand in for the agent so you can run it without any agent installed: `fake-agent.ps1` prints four lines and then `DONE`. Replace `$agent` with `claude`, `codex`, `aider --message "..."` or whatever you run. ```powershell # fake-agent.ps1 (stand in for a real agent) 1..4 | ForEach-Object { Write-Host "step $_"; Start-Sleep 1 } Write-Host DONE ``` ```powershell $ErrorActionPreference = 'Stop' $s = 'work' $logDir = Join-Path $env:TEMP 'agent-logs' New-Item -ItemType Directory -Force $logDir | Out-Null $agent = "pwsh -NoProfile -File $env:TEMP/fake-agent.ps1" # real life: 'claude' or 'codex' psmux new-session -d -s $s -n hub -x 160 -y 45 psmux set-option -t $s remain-on-exit on # keep a finished or crashed pane visible $names = 'api', 'web', 'tests' foreach ($n in $names) { psmux new-window -t $s -n $n -d psmux select-pane -t "${s}:${n}.0" -T "agent: $n" psmux pipe-pane -t "${s}:${n}.0" -o "cat > $logDir\$n.log" psmux send-keys -t "${s}:${n}.0" $agent Enter } # Poll every agent window until its pane prints DONE $pending = @($names) $deadline = (Get-Date).AddSeconds(600) while ($pending.Count -gt 0 -and (Get-Date) -lt $deadline) { foreach ($n in @($pending)) { $out = psmux capture-pane -t "${s}:${n}.0" -p if ($out -match '^DONE') { Write-Host "$n finished" $pending = @($pending | Where-Object { $_ -ne $n }) } } Start-Sleep -Milliseconds 500 } psmux list-windows -t $s -F '#{window_index}:#{window_name} #{pane_title} cmd=#{pane_current_command}' foreach ($n in $names) { psmux pipe-pane -t "${s}:${n}.0" } # stop logging Get-ChildItem $logDir | Select-Object Name, Length ``` Output of a real run (6.6 seconds end to end, including three new windows and three shells): ``` api finished web finished tests finished 0:hub SUPERFLOW cmd=pwsh 1:api agent: api cmd=pwsh 2:web agent: web cmd=pwsh 3:tests agent: tests cmd=pwsh Name Length ---- ------ api.log 1322 tests.log 1316 web.log 1322 ``` The `hub` window's title is whatever the shell last set with an OSC title sequence (here the machine name). See [Pane Titles](../pane-titles.md) if you want to stop PowerShell from doing that. ### Sending a prompt to a running agent `send-keys` types into the pane exactly as you would. A prompt for an interactive agent is one string and an `Enter`: ```powershell psmux send-keys -t work:api 'Add input validation to the /users endpoint and run the tests' Enter ``` Two things to know: - Quotes and backslashes in the string reach the pane as typed, but a command that nests quotes inside `pwsh -Command "..."` inside a PowerShell string is hard to read and easy to get wrong. Put such a command in a `.ps1` file and send `pwsh -File ...` instead. That is what the script above does. - `send-keys -l` sends the string literally with no key name parsing, useful when a prompt contains a word like `Enter` or `Space`. ### Reading what an agent printed `capture-pane -p` returns the visible screen. Add `-S -200` to include the last 200 lines of scrollback, or `-S -` for all of it. `-J` joins lines that wrapped. ```powershell psmux capture-pane -t work:api -p -S -200 | Select-String -Pattern 'error|failed' ``` If you need the complete transcript with colours, read the `pipe-pane` log file instead. If you only need to know whether the agent is still running, ask the server: ```powershell psmux list-panes -t work:api -F 'dead=#{pane_dead} cmd=#{pane_current_command} pid=#{pane_pid}' ``` `#{pane_dead}` is `1` once the process has exited and `remain-on-exit` kept the pane. `#{pane_current_command}` is the foreground process, so it reads `claude`, `node`, `nvim` or `pwsh` depending on what is in front. ## Restarting a crashed agent With `remain-on-exit on` a pane whose process died stays on screen showing its last output. Restart it in place, with a different command if you like: ```powershell # Kill whatever is left and start the agent again in the same pane psmux respawn-pane -k -t work:api -- claude # Or bring back the default shell psmux respawn-pane -k -t work:api ``` Checked live with a pane whose command had exited: ``` PS> psmux list-panes -t work:worker -F '#{pane_index} dead=#{pane_dead}' 0 dead=1 PS> psmux respawn-pane -k -t work:worker -- pwsh -NoProfile -Command Start-Sleep 60 PS> psmux list-panes -t work:worker -F '#{pane_index} dead=#{pane_dead} cmd=#{pane_current_command}' 0 dead=0 cmd=pwsh ``` A supervisor loop is a few lines of PowerShell: poll `#{pane_dead}` every few seconds and `respawn-pane -k` when it turns to `1`. The layout, the pane id, the log pipe and the pane's scrollback are all preserved across the respawn, so a restarted agent's pane still shows what the previous run printed above the new output. `-e KEY=VALUE` hands the new process its own environment, for example an identity for a successor agent. ## Showing the active agent in the status line Because every pane can carry a title, the status line can name the agent that has focus: ```tmux set -g status-right '#{pane_title} [#{pane_current_command}]' ``` ``` PS> psmux display-message -t work:agent.0 -p '#{pane_title} [#{pane_current_command}]' agent: claude [pwsh] ``` Window names do the same job for the tab list. `new-window -n api` names the tab and turns `automatic-rename` off for that window, so the name stays put when the agent starts a child process. If you rename a window later with `rename-window`, the same lock applies. ## A quick shell without leaving the agent `display-popup` opens a floating pane over the current window and closes it when the command exits: ```powershell psmux display-popup -w 60% -h 50% -E 'pwsh -NoProfile' ``` Bind it if you use it often: ```tmux bind-key g display-popup -w 80% -h 80% -E 'lazygit' ``` Zoom is the other way to focus: `prefix + z` (or `resize-pane -Z`) makes the agent pane fill the window and again to go back. ## Claude Code teams in psmux panes Claude Code has a teammate mode that spawns each teammate agent in its own tmux pane via `split-window` and `send-keys`. psmux sets the environment Claude Code needs (`TMUX`, `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`) and injects `--teammate-mode tmux` when you have not configured it yourself, so a plain `claude` in a psmux pane produces visible teammates. That page also explains the second, in process worktree agent system that never opens panes and why Opus tends to choose it. Read [Claude Code agent teams](../claude-code.md) before relying on it. Two open issues are worth knowing about: [#578](https://github.com/psmux/psmux/issues/578) (the `TMUX` variable is lost when Claude Code enters Agent View) and [#580](https://github.com/psmux/psmux/issues/580) (pane id targets and silent respawn failures in the teammate backend). Check their current state on GitHub if teammates do not appear. ## Running TUIs: neovim, yazi, lazygit, btop, k9s A TUI in a psmux pane is a normal console program in a normal ConPTY. Launch it from the shell in the pane, or make it the pane's command: ```powershell psmux new-window -t work -n edit -- nvim . psmux new-window -t work -n files -- yazi psmux new-window -t work -n git -- lazygit ``` `#{pane_current_command}` reports the TUI while it runs, which makes `automatic-rename` and the status line useful: ``` PS> psmux new-window -t work -n edit -d -- nvim -u NONE PS> psmux list-panes -t work:edit -F 'cmd=#{pane_current_command}' cmd=nvim ``` ### The same TUIs inside WSL Start a WSL shell in a pane and run Linux builds of the same tools. Everything renders through the same ConPTY, colours and box drawing included: ```powershell psmux new-window -t work -n linux -- wsl -d Ubuntu psmux send-keys -t work:linux 'htop' Enter ``` ``` PS> psmux send-keys -t work:linux 'echo from-wsl $(uname -r)' Enter PS> psmux capture-pane -t work:linux -p | Select-Object -Last 2 from-wsl 6.6.114.1-microsoft-standard-WSL2 user@host:/mnt/c/Users/you/project$ PS> psmux list-panes -t work:linux -F 'cmd=#{pane_current_command}' cmd=wslhost ``` Note the last line: Windows cannot see inside the WSL VM, so `#{pane_current_command}` reads `wslhost` regardless of what Linux program is running, and `#{pane_current_path}` cannot follow a Linux `cd` on its own. Since [#615](https://github.com/psmux/psmux/issues/615) a Linux shell can announce its working directory with an OSC 7 sequence; see [Multi Shell](../multi-shell.md) for the setup. To make WSL the default shell for every new pane, see [Setting Your Default Shell](../multi-shell.md#setting-your-default-shell). ### What the mouse wheel does in a TUI The rule is tmux's rule: a program that turned mouse reporting on gets the wheel (and clicks and drags); a program that did not gets nothing, and psmux scrolls its own scrollback instead. So `:set mouse=a` in neovim, `--mouse` for `less`, and the mouse setting in htop are what make the wheel work inside those programs. The reasons, the Windows 10 caveat for programs that read the mouse as VT bytes on stdin (Claude Code among them), and the SSH cases are in the [FAQ](../faq.md) and [Mouse Over SSH](../mouse-ssh.md). If you want psmux's own drag to copy selection even inside a mouse aware TUI, `set -g mouse-selection-force on` keeps it; see [Configuration](../configuration.md). ## Putting it together: an agent supervisor session A combined example: one window per agent, each logged, a `hub` window whose shell runs the supervisor loop, and a status line that shows what is in front. ```powershell $s = 'agents' psmux new-session -d -s $s -n hub -x 200 -y 50 psmux set-option -t $s remain-on-exit on psmux set-option -t $s status-right '#{pane_title} [#{pane_current_command}] %H:%M' foreach ($n in 'backend', 'frontend', 'docs') { psmux new-window -t $s -n $n -d -c "C:\src\project\$n" psmux select-pane -t "${s}:${n}.0" -T "agent: $n" psmux pipe-pane -t "${s}:${n}.0" -o "cat >> C:\logs\$n.log" psmux send-keys -t "${s}:${n}.0" 'claude' Enter } psmux attach -t $s ``` From another terminal, or from a scheduled task, the same session is fully inspectable: `psmux list-windows -t agents`, `psmux capture-pane -t agents:backend -p`, `psmux respawn-pane -k -t agents:docs -- claude`. Detach with `prefix + d`; the agents keep running. ## FAQ ### Do I need WSL or Cygwin to run agents this way on Windows? No. psmux is a native Windows program. The agents run as Windows processes in ConPTY panes. WSL is optional and only needed for Linux only tools, and a WSL shell fits in a pane like any other shell. ### Can one agent drive other agents through psmux? Yes. Everything on this page is a CLI command, so an agent with shell access can run `psmux new-window`, `send-keys` and `capture-pane` itself. Claude Code's teammate mode is exactly this, built in; see [Claude Code agent teams](../claude-code.md). ### Will my tmux scripts for agents work unchanged? Mostly. The command names, flags, targets and format variables are tmux's. Use `tmux` as the command name (psmux installs a `tmux.exe` alias) and keep paths and shell syntax portable. The one Windows specific idiom above is the `cat > file` sink for `pipe-pane`, which works on both platforms. See [Cross Platform tmux Scripts](cross-platform-tmux-scripts.md) and [Compatibility](../compatibility.md). ### How do I know an agent has finished? Poll `capture-pane -p` for a marker the agent prints, or poll `#{pane_dead}` if the agent exits when done. Both are shown above. For a running agent that goes idle rather than exiting, `#{pane_current_command}` returning to the shell name is another signal. ### What happens to running agents when I close Windows Terminal? Nothing. They live in the psmux server process, not in the window. `psmux attach -t ` from any new terminal brings them back. See [Windows Use Cases](../use-cases.md) for boot time services and remote reattach. ### Does the mouse work in Claude Code, Codex and other node based TUIs? On Windows 11 build 22523 and newer, yes, once the program enables mouse reporting. On Windows 10 the VT mouse bytes are lost inside conhost for programs that read stdin; the [FAQ](../faq.md) has the measured details and the list of frameworks that are unaffected. --- # Multi Window and Multi Pane Development Layouts on Windows with psmux This tutorial shows how to design a project session in psmux, the native tmux for Windows: one window per concern (editor, server, tests, git), panes sized and arranged with tmux layout commands, a bootstrap script that rebuilds the whole thing in a second, and a Windows Terminal profile that opens straight into it. It is written for developers on Windows who want the multi pane, multi window workflow tmux users have on Linux and macOS, using PowerShell, cmd, Git Bash or WSL inside the panes. Every command was run against psmux 3.3.8 on Windows 11 with PowerShell 7 and Windows Terminal. `psmux`, `pmux` and `tmux` are the same binary. ## What you will learn - How to structure a project session into windows and panes that map to what you actually do - `split-window` sizing with `-l 30%` and `-p 25`, and the five `select-layout` presets - Rearranging on the fly: `swap-pane`, `rotate-window`, `resize-pane`, `break-pane`, zoom, `move-window` - `synchronize-panes` for running one command in several panes at once - Window naming, `automatic-rename`, `respawn-window`, and logging a pane with `pipe-pane` - A bootstrap script in PowerShell and the same one in bash, and a Windows Terminal profile that lands in the session - Four complete layouts to copy Related pages: [Scripting and Automation](../scripting.md) for every command and format variable, [Command Reference](../tmux_args_reference.md) for per command flags, [Cross Platform tmux Scripts](cross-platform-tmux-scripts.md) for making the bootstrap portable, [Running Terminal AI Agents and TUIs](terminal-agents-and-tuis.md) for agent focused layouts, and [Live Preview in Choosers](../preview.md) for navigating between sessions. ## Designing a project session A session is a named workspace that lives in the psmux server. Windows are the tabs in its status line; panes are the splits inside a window. A layout that works for most projects: | Window | Contents | Why its own window | |---|---|---| | `edit` | editor pane plus a narrow shell for quick commands | the thing you look at most gets the most space | | `server` | dev server, watcher, or docker compose logs | long running, noisy, rarely typed into | | `tests` | a shell for running tests, a small pane tailing test output | you switch here, run, read, switch back | | `git` | lazygit, gitui, or a plain shell | commits and reviews without disturbing the editor | Naming the windows is the difference between `prefix + 2` meaning something and meaning nothing. `new-window -n server` names the tab and turns `automatic-rename` off for that window, so the name survives whatever process starts inside. ## Splitting and sizing panes `split-window -h` puts the new pane to the right, `-v` puts it below. `-l` takes a size in cells or a percentage; `-p` takes a percentage. `-d` keeps focus where it was. `-c` sets the start directory. ```powershell psmux new-session -d -s proj -n edit -x 160 -y 45 psmux split-window -t proj:edit -h -l 30% -d # 30% column on the right psmux split-window -t proj:edit.0 -v -l 25% -d # 25% strip under the left pane psmux list-panes -t proj:edit -F '#{pane_index} #{pane_id} #{pane_width}x#{pane_height} at #{pane_left},#{pane_top}' ``` ``` 0 %1 111x33 at 0,0 1 %3 111x11 at 0,34 2 %2 48x45 at 112,0 ``` `-p 25` gives the same 25 percent strip as `-l 25%`. Both forms are accepted, so a script written for either tmux 2.x (`-p`) or 3.x (`-l N%`) works unchanged. Pane ids (`%1`, `%2`, ...) are allocated per server and never reused within a run, which makes them the safest target for scripts. Pane indexes (`0`, `1`, `2`) are positional and change when panes are swapped or closed. ## The five layout presets `select-layout` arranges the panes of a window. With four panes in a 160x45 window: ``` PS> foreach ($l in 'even-horizontal','even-vertical','main-horizontal','main-vertical','tiled') { >> psmux select-layout -t proj:grid $l >> "$l -> " + (psmux display-message -t proj:grid -p '#{window_layout}') >> } even-horizontal -> 2b42,160x45,0,0{39x45,0,0,4,39x45,40,0,8,39x45,80,0,7,40x45,120,0,6} even-vertical -> 9fb8,160x45,0,0[160x10,0,0,4,160x10,0,11,8,160x10,0,22,7,160x12,0,33,6] main-horizontal -> 0afc,160x45,0,0[160x26,0,0,4,160x18,0,27{52x18,0,27,8,52x18,53,27,7,54x18,106,27,6}] main-vertical -> 5202,160x45,0,0{95x45,0,0,4,64x45,96,0[64x14,96,0,8,64x14,96,15,7,64x15,96,30,6]} tiled -> e59f,160x45,0,0[160x22,0,0{79x22,0,0,4,80x22,80,0,8},160x22,0,23{79x22,0,23,7,80x22,80,23,6}] ``` ``` even-horizontal even-vertical main-horizontal main-vertical tiled +---+---+---+---+ +---------------+ +---------------+ +---------+-----+ +-------+-------+ | | | | | +---------------+ | main | | | | | | | | | | | | +---------------+ +----+----+-----+ | main +-----+ +-------+-------+ | | | | | +---------------+ | | | | | | | | | | +---+---+---+---+ +---------------+ +----+----+-----+ +---------+-----+ +-------+-------+ ``` `next-layout` (`prefix + Space`) cycles through them. The strings printed above are tmux layout strings: capture `#{window_layout}` after arranging a window by hand and feed it back to `select-layout` later to restore that arrangement. The aliases `even-h`, `even-v`, `main-h` and `main-v` are accepted too. ## Rearranging on the fly ```powershell psmux swap-pane -s proj:grid.0 -t proj:grid.1 # exchange two panes psmux rotate-window -t proj:grid # every pane moves one slot psmux resize-pane -t proj:grid.0 -L 10 # 10 cells narrower on the left edge psmux resize-pane -t proj:grid.0 -D 5 # 5 cells taller downwards psmux resize-pane -t proj:grid.0 -Z # zoom (again to unzoom), same as prefix + z psmux break-pane -d -t proj:grid.3 # pane 3 becomes its own window, focus stays psmux move-window -s proj:git -t proj:9 # renumber a window ``` Checked live, the swap and the rotation do what the names say: ``` PS> psmux swap-pane -s proj:grid.0 -t proj:grid.1; psmux list-panes -t proj:grid -F '#{pane_index} #{pane_id}' | Select-Object -First 2 0 %8 1 %4 PS> psmux rotate-window -t proj:grid; psmux list-panes -t proj:grid -F '#{pane_index} #{pane_id}' 0 %4 1 %7 2 %6 3 %8 ``` Zoom survives a split. `split-window -Z` while a pane is zoomed keeps the window zoomed on the new pane ([PR 572](https://github.com/psmux/psmux/pull/572)): ``` PS> psmux resize-pane -t proj:grid.0 -Z; psmux display-message -t proj:grid -p 'zoomed=#{window_zoomed_flag}' zoomed=1 PS> psmux split-window -t proj:grid -Z -d; psmux display-message -t proj:grid -p 'zoomed=#{window_zoomed_flag} panes=#{window_panes}' zoomed=1 panes=5 ``` Moving a pane from one window into another is `join-pane -s ` (`move-pane` is the same command); its cross session form is described in [Scripting and Automation](../scripting.md). ## One command in every pane: `synchronize-panes` Turn it on for a window and every keystroke goes to all of its panes. Handy for running the same build in several checkouts, or the same command on several SSH hosts. ```powershell psmux set-option -t proj:grid -w synchronize-panes on psmux send-keys -t proj:grid.0 'git pull' Enter psmux set-option -t proj:grid -w synchronize-panes off ``` ``` PS> psmux send-keys -t proj:grid.0 'Write-Host "sync $PID"' Enter PS> 0..3 | ForEach-Object { "pane $_ : " + (psmux capture-pane -t proj:grid.$_ -p | Select-String '^sync').Count } pane 0 : 1 pane 1 : 1 pane 2 : 1 pane 3 : 1 PS> psmux display-message -t proj:grid -p 'synchronized=#{pane_synchronized}' synchronized=1 ``` `toggle-sync` is a psmux shortcut for flipping it from a key binding: `bind-key S toggle-sync`. ## Logging a pane and restarting a window `pipe-pane -o "cat > "` streams a pane's raw output to a file; the psmux server writes the file itself so the idiom works even though PowerShell's `cat` is `Get-Content`. Run it again with no `-o` to stop. Details in [Piping Pane Output](../scripting.md#piping-pane-output-pipe-pane). ```powershell psmux pipe-pane -t proj:server -o "cat >> C:\logs\devserver.log" ``` When a window's process has exited (with `remain-on-exit on` the pane stays visible), `respawn-window` restarts it in place: ``` PS> psmux list-panes -t proj:server -F 'dead=#{pane_dead}' dead=1 PS> psmux respawn-window -t proj:server PS> psmux list-panes -t proj:server -F 'dead=#{pane_dead} cmd=#{pane_current_command}' dead=0 cmd=pwsh ``` `respawn-pane -k -- ` does the same for one pane and lets you change the command; see [Dead Panes and Respawn](../configuration.md#dead-panes-and-respawn). ## Window names and `automatic-rename` `new-window -n name` and `rename-window` both lock the name. `show-options -w automatic-rename` on that window reports `off` afterwards: ``` PS> psmux rename-window -t proj:grid workers PS> psmux show-options -t proj:workers -w automatic-rename automatic-rename off ``` Windows without a fixed name track the foreground process (`pwsh`, `nvim`, `node`) through `#{pane_current_command}`. To hand a renamed window back to automatic naming: `psmux set-option -t proj:workers -w automatic-rename on`. ## The bootstrap script A session that has to be rebuilt by hand is a session you stop using. Put the layout in a script, guard it with `has-session`, and end with `attach`. This one creates the four window layout from the top of the page. PowerShell (`proj.ps1`): ```powershell $s = 'proj' $root = 'C:\src\proj' psmux has-session -t $s 2>$null if ($LASTEXITCODE -ne 0) { psmux new-session -d -s $s -n edit -c $root -x 160 -y 45 psmux split-window -t "${s}:edit" -h -l 35% -c $root psmux new-window -t $s -n server -c $root -d psmux new-window -t $s -n tests -c $root -d psmux split-window -t "${s}:tests" -v -l 30% -c $root -d psmux new-window -t $s -n git -c $root -d psmux select-window -t "${s}:edit" psmux select-pane -t "${s}:edit.0" psmux send-keys -t "${s}:edit.0" 'nvim .' Enter psmux send-keys -t "${s}:server" 'npm run dev' Enter psmux send-keys -t "${s}:git" 'lazygit' Enter } psmux attach -t $s ``` The same script in bash, which runs unchanged under tmux on Linux and macOS and under psmux on Windows (Git Bash or WSL) because psmux ships a `tmux.exe` alias: ```bash #!/usr/bin/env bash s=proj root="$HOME/src/proj" tmux has-session -t "$s" 2>/dev/null || { tmux new-session -d -s "$s" -n edit -c "$root" -x 160 -y 45 tmux split-window -t "$s:edit" -h -l 35% -c "$root" tmux new-window -t "$s" -n server -c "$root" -d tmux new-window -t "$s" -n tests -c "$root" -d tmux split-window -t "$s:tests" -v -l 30% -c "$root" -d tmux new-window -t "$s" -n git -c "$root" -d tmux select-window -t "$s:edit" } tmux attach -t "$s" ``` What both produce (the `send-keys` lines omitted for the check): ``` PS> psmux list-windows -t proj -F '#{window_index}:#{window_name} panes=#{window_panes} active=#{window_active}' 0:edit panes=2 active=1 1:server panes=1 active=0 2:tests panes=2 active=0 3:git panes=1 active=0 PS> psmux list-panes -t proj:edit -F '#{pane_index} #{pane_width}x#{pane_height} #{pane_current_path}' 0 103x45 C:\src\proj 1 56x45 C:\src\proj ``` Portability rules (paths, quoting, which shell the panes run) are covered in [Cross Platform tmux Scripts](cross-platform-tmux-scripts.md). ### Saving and restoring layouts automatically If you would rather not maintain a script, the psmux-resurrect plugin saves every session's windows, panes, layouts and directories, and psmux-continuum saves on a timer and restores on the next start. Both are listed with installation steps in [Plugins and Themes](../plugins.md). ## A Windows Terminal profile that opens the session `new-session -A` attaches if the session exists and creates it otherwise, so one command is both "start" and "resume": ```powershell psmux new-session -A -s proj -c C:\src\proj ``` Checked live: the first call created the session with `#{pane_current_path}` reporting `C:\src\proj`; a second call attached to it without creating a duplicate (`list-sessions` still showed one `proj`). Add it as a profile in Windows Terminal's `settings.json`: ```json { "name": "proj", "commandline": "pwsh.exe -NoLogo -Command \"psmux new-session -A -s proj -c C:\\src\\proj\"", "startingDirectory": "C:\\src\\proj" } ``` If you want the profile to run the full bootstrap script instead, point `commandline` at `pwsh.exe -File C:\src\proj\proj.ps1`; the script's `has-session` guard makes it safe to open the profile twice. ## Switching between sessions `prefix + s` opens `choose-session`, `prefix + w` opens the window chooser, `psmux choose-tree` (from the CLI or the `prefix + :` prompt) shows every session, window and pane, and `p` toggles a live preview of the highlighted target so you can see a window before you jump. `set -g choose-tree-preview on` opens the preview by default. See [Live Preview in Choosers](../preview.md). ## Four layouts to copy Each block creates a detached session; add `psmux attach -t ` at the end or open it from a profile as above. ### 1. Editor with a side terminal ``` +-----------------------------------+------------+ | | | | nvim / code | shell | | | | | | | +-----------------------------------+------------+ ``` ```powershell psmux new-session -d -s edit -n main -c C:\src\proj -x 160 -y 45 psmux split-window -t edit:main -h -l 30% -c C:\src\proj -d psmux send-keys -t edit:main.0 'nvim .' Enter ``` ### 2. Full stack: server, client, tests, shell ``` +-------------------------+-------------------------+ | api server | web dev server | | | | +-------------------------+-------------------------+ | tests (watch) | shell | | | | +-------------------------+-------------------------+ ``` ```powershell psmux new-session -d -s stack -n dev -c C:\src\proj -x 160 -y 45 psmux split-window -t stack:dev -v -c C:\src\proj -d # tests, bottom left psmux split-window -t stack:dev.0 -h -c C:\src\proj\web -d # web, top right psmux split-window -t stack:dev.2 -h -c C:\src\proj -d # shell, bottom right psmux select-layout -t stack:dev tiled psmux select-pane -t stack:dev.0 -T 'api' psmux select-pane -t stack:dev.1 -T 'web' psmux select-pane -t stack:dev.2 -T 'tests' psmux select-pane -t stack:dev.3 -T 'shell' psmux set-option -t stack pane-border-status top psmux set-option -t stack pane-border-format ' #{pane_title} ' ``` ### 3. Ops dashboard: main log plus three monitors ``` +---------------------------------------------------+ | main: application log (pipe-pane to a file) | | | +----------------+----------------+-----------------+ | btop | docker stats | k9s / shell | +----------------+----------------+-----------------+ ``` ```powershell psmux new-session -d -s ops -n dash -x 160 -y 45 psmux split-window -t ops:dash -v -d psmux split-window -t ops:dash.1 -h -d psmux split-window -t ops:dash.1 -h -d psmux select-layout -t ops:dash main-horizontal psmux pipe-pane -t ops:dash.0 -o "cat >> C:\logs\app.log" psmux send-keys -t ops:dash.1 'btop' Enter psmux send-keys -t ops:dash.2 'docker stats' Enter ``` ### 4. Multi checkout with synchronized panes ``` +-------------------------+-------------------------+ | repo A | repo B | +-------------------------+-------------------------+ | repo C | repo D | +-------------------------+-------------------------+ synchronize-panes on: one "git pull && npm test" runs in all four ``` ```powershell psmux new-session -d -s multi -n repos -c C:\src\a -x 160 -y 45 psmux split-window -t multi:repos -v -c C:\src\c -d # bottom left psmux split-window -t multi:repos.0 -h -c C:\src\b -d # top right psmux split-window -t multi:repos.2 -h -c C:\src\d -d # bottom right psmux select-layout -t multi:repos tiled psmux set-option -t multi:repos -w synchronize-panes on ``` ## FAQ ### Can I keep different shells in different panes of one layout? Yes. `split-window` and `new-window` take a command after `--`, so one pane can be `pwsh`, the next `cmd.exe`, the next `wsl -d Ubuntu`, and `default-shell` sets the fallback. See [Multi Shell](../multi-shell.md). ### Will the layout survive closing Windows Terminal? Yes. The session, its windows, panes and running processes live in the psmux server. Reopen a terminal and `psmux attach -t proj` (or the `-A` profile above) puts you back. Sign out and the server is gone, which is where psmux-resurrect and psmux-continuum come in. ### Do tmux layout strings from Linux work here? Yes, the format is the same. Capture `#{window_layout}` on either platform and pass it to `select-layout`. Pane counts must match; the string describes geometry, not contents. ### How do I target a pane in a script without guessing its index? Use the pane id printed by `split-window -P -F '#{pane_id}'` or by `list-panes -F '#{pane_id}'`. Ids like `%7` are stable for the pane's lifetime; indexes change when you swap or close panes. ### Why does my sized split come out a few cells different from what I asked? Borders take a cell, and percentages are rounded to whole cells. Asking for `-l 30%` of a 160 column window gives a 48 column pane plus a one cell border, as the `list-panes` output at the top of the page shows. --- # Configuration psmux reads its config on startup from the **first file found** (in order): 1. `~/.psmux.conf` 2. `~/.psmuxrc` 3. `~/.tmux.conf` 4. `~/.config/psmux/psmux.conf` Config syntax is **tmux-compatible**. Most `.tmux.conf` lines work as-is. You can also specify a custom config file path with the `-f` flag: ```powershell # Use a specific config file instead of default search psmux -f ~/.config/psmux/custom.conf # Use an empty config (no settings loaded) psmux -f NUL ``` This sets the `PSMUX_CONFIG_FILE` environment variable internally, which the server checks before searching the default locations. ## Basic Config Example Create `~/.psmux.conf`: ```tmux # Change prefix key to Ctrl+a set -g prefix C-a # Enable mouse set -g mouse on # Window numbering base (default is 0) set -g base-index 1 # Customize status bar set -g status-left "[#S] " set -g status-right "%H:%M %d-%b-%y" set -g status-style "bg=green,fg=black" # Cursor shape. Leave both out, as most configs do, and psmux asks for no # shape, so the cursor you had before starting it is the cursor you keep. set -g cursor-style bar set -g cursor-blink on # Scrollback history set -g history-limit 5000 # Prediction dimming (disable for apps like Neovim) set -g prediction-dimming off # Key bindings bind-key -T prefix h split-window -h bind-key -T prefix v split-window -v ``` ## Choosing a Shell psmux launches **PowerShell 7 (pwsh)** by default, whatever shell you typed `psmux` into. Like tmux, it does not look at the launching shell; it looks at the environment. If the `SHELL` environment variable names a shell psmux can start (an absolute path to an `.exe`, `.cmd`, `.bat` or `.com` that exists, or a bare name such as `powershell` that resolves on `PATH`), that shell becomes the initial `default-shell`, exactly as tmux seeds `default-shell` from `$SHELL`. A `SHELL` that is not a Windows path, does not exist, or points at psmux itself is ignored and the usual walk (`pwsh`, then `powershell`, then `cmd`) applies. One consequence worth knowing: a login Git Bash (the Windows Terminal Git Bash profile runs `bash.exe -i -l`) hands Windows children `SHELL=C:\Program Files\Git\usr\bin\bash.exe`, so `psmux` typed into that tab opens bash panes, just as tmux would. Setting `default-shell` in your config overrides both. You can change this: ```tmux # Use cmd.exe set -g default-shell cmd # Use PowerShell 5 (Windows built-in) set -g default-shell powershell # Use PowerShell 7 (explicit path) set -g default-shell "C:/Program Files/PowerShell/7/pwsh.exe" # Use Git Bash set -g default-shell "C:/Program Files/Git/bin/bash.exe" # Use Nushell set -g default-shell nu # Use Windows Subsystem for Linux (via wsl.exe) set -g default-shell wsl ``` You can also launch a window with a specific command without changing the default: ```powershell psmux new-window -- cmd /K echo hello psmux new-session -s py -- python psmux split-window -- "C:/Program Files/Git/bin/bash.exe" ``` ## Quoting Option Values A quoted option value is stored exactly as written, including runs of spaces and any leading or trailing space. This is the same in a config file as it is on the command line: ```bash # Both of these store the twelve spaces set -g status-right "left right" ``` ```bash psmux set -g status-right "left right" ``` Details worth knowing: - Quotes are only stripped when they wrap the whole value. `a"b"c` stores `a"b"c` literally. - Inside double quotes, `\"` and `\\` are unescaped. Inside single quotes nothing is unescaped, so `'a\b'` stores `a\b`. - An unquoted value is taken as written but with trailing whitespace removed. - A trailing `# comment` is stripped, unless the `#` is inside quotes or escaped, so `"#{session_name}"` and `"#[fg=red]"` are safe. - `#{p:}` still pads at render time and remains useful for padding that should follow the rendered width rather than a fixed number of spaces. - Flags are only parsed before the option name. `set -g @k -u` stores the value `-u` rather than unsetting `@k`, and a value that starts with a dash can also be written after `--`, the tmux end of options marker: `psmux set-option -g -- status-left "-> "`. - `show-option` and `show-window-option` are accepted as singular spellings of `show-options` and `show-window-options`, as they are in tmux. ## All Set Options As in tmux, a name that is neither in this table, nor in tmux's own options table, nor a `@user` option is refused: `set-option` and `show-options` fail with `invalid option: NAME`, and `-q` makes that a silent success that sets nothing. In a config file it is reported as `unknown option 'NAME'` and skipped. A tmux option psmux does not implement (`visual-silence`, `lock-command` and so on) is accepted and read back as text. Use `set-environment`, not `set-option`, for environment variables. | Option | Type | Default | Description | |--------|------|---------|-------------| | `prefix` | Key | `C-b` | Prefix key | | `prefix2` | Key | `none` | Secondary prefix key (optional) | | `base-index` | Int | `0` | First window number | | `pane-base-index` | Int | `0` | First pane number | | `escape-time` | Int | `10` | Escape delay (ms), tmux 3.5+ default | | `repeat-time` | Int | `500` | How long a key bound with `bind-key -r` keeps repeating without the prefix, in ms. Range `0` to `2000000`; `0` disables repeat | | `history-limit` | Int | `2000` | Scrollback lines per pane | | `priority` | Str | `above-normal` | Scheduling class for psmux's own processes: `normal`, `above-normal`, `high`. Pane shells and the programs you run are never raised. See [Process Priority](#process-priority) | | `display-time` | Int | `750` | Message display time (ms) | | `display-panes-time` | Int | `1000` | Pane overlay time (ms) | | `status-interval` | Int | `15` | Status refresh (seconds) | | `mouse` | Bool | `on` | Mouse support | | `mouse-selection` | Bool | `on` | psmux's client-side drag selection. Set `off` to let in-pane TUI apps (opencode, nvim, etc.) handle their own mouse selection without psmux drawing on top | | `mouse-selection-force` | Bool | `off` | Keep psmux drag selection active in apps that request mouse tracking. Plain clicks are replayed to the app; drags are copied by psmux | | `scroll-enter-copy-mode` | Bool | `on` | Enter copy mode on mouse scroll (set `off` to disable) | | `ctrl-wheel-zoom` | Bool | `off` | Windows Terminal only: Ctrl+Wheel zooms the terminal font one step per notch instead of scrolling the pane. psmux has mouse reporting on, so Windows Terminal forwards Ctrl+Wheel to it; with this on, psmux sends Windows Terminal its default zoom keys (Ctrl+Numpad Plus / Minus) back. Does nothing over SSH, in other terminals, or when `experimental.scrollToZoom` is `false` in that terminal's settings | | `mouse-drag-enter-copy-mode` | Bool | `off` | Enter copy mode and select there on a left-button drag in a pane that does not track the mouse (tmux's `MouseDragStart` → `copy-mode -M`) instead of the psmux client-side selection overlay | | `pwsh-mouse-selection` | Bool | `off` | tmux-like release-copy selection with word/line multi-click and pane-clipped extraction | | `paste-detection` | Bool | `on` | Reassemble a terminal's Ctrl+V paste into one bracketed paste. A Ctrl+V the terminal does not paste on reaches bindings and the pane as C-v either way (set `off` to forward every key as typed) | | `choose-tree-preview` | Bool | `off` | Open `choose-session` / `choose-tree` pickers with the live preview pane already visible (saves pressing `p`). See [preview.md](preview.md) | | `bold-is-bright` | Bool | `on` | Restore standard SGR codes for the 16 basic colors so the outer terminal renders `bold` as bright (matches a bare shell). Set `off` to keep explicit 256-indexed low colors byte-accurate. See [Bold Is Bright](#bold-is-bright-color-rendering) | | `status` | Bool/Int | `on` | Show status bar (number = line count) | | `status-position` | Str | `bottom` | `top` or `bottom` | | `status-justify` | Str | `left` | `left`, `centre`, `right`, `absolute-centre` | | `status-left-length` | Int | `10` | Max width of status-left | | `status-right-length` | Int | `40` | Max width of status-right | | `focus-events` | Bool | `off` | Pass focus events to apps | | `program-status` | Bool | `off` | psmux extension. Accept OSC 7501 program status reports and answer its capability query. Off, OSC 7501 is ignored and the query draws no reply. See [program-status.md](program-status.md) | | `alternate-screen` | Bool | `on` | Honour the DEC 47 / 1049 alternate screen. Set `off` so full screen program output lands in the scrollback instead of being discarded on exit. See [Alternate Screen](#alternate-screen) | | `mode-keys` | Str | `emacs` | `vi` or `emacs` | | `status-keys` | Str | | Editing style at the command prompt. Only the literal `vi` is checked; any other value (including unset) keeps emacs style editing | | `copy-mode-line-numbers` | Str | `off` | Line number gutter in copy mode: `off`, `default`, `absolute`, `relative`, `hybrid`. See [Copy Mode Line Numbers](#copy-mode-line-numbers) | | `wrap-search` | Bool | `on` | Wrap copy-mode searches around the ends of the scrollback. Set `off` to stop at the first or last match | | `renumber-windows` | Bool | `off` | Auto-renumber windows on close | | `automatic-rename` | Bool | `on` | Rename windows from foreground process | | `monitor-activity` | Bool | `off` | Flag windows with new output | | `monitor-silence` | Int | `0` | Seconds before silence flag (0=off) | | `visual-activity` | Bool | `off` | Visual indicator for activity | | `synchronize-panes` | Bool | `off` | Send input to all panes | | `remain-on-exit` | Bool | `off` | Keep panes after process exits | | `@kill-descendants` | Bool | `on` | Terminate a self-exited pane shell's background children (psmux extension) | | `@mouse-force` | Bool | `off` | Pane scoped only (`set-option -p -t %N @mouse-force on`). Forward the wheel into this pane even though its application registered for the mouse through neither channel psmux can see. psmux normally forwards only to a pane that asked, either by sending a mouse DECSET or by holding `ENABLE_MOUSE_INPUT` on its console, and it keeps that authorization for as long as the process that earned it is alive, so a `node` child entering raw mode can no longer take it away. Some applications never register through either channel on Windows: conhost can swallow their DECSET before psmux parses it, and libuv raw mode leaves their console without the mouse bit. Set this for that pane. Leave it off for panes running programs that do not read the mouse, which receive the report as literal keystrokes (psmux extension) | | `aggressive-resize` | Bool | `off` | Resize to smallest client | | `window-size` | Str | `latest` | `largest`, `smallest`, `manual`, `latest` | | `destroy-unattached` | Bool | `off` | Exit server when no clients attached | | `exit-empty` | Bool | `on` | Exit server when all windows closed | | `set-titles` | Bool | `off` | Update terminal title | | `set-titles-string` | Str | | Terminal title format | | `default-shell` | Str | `$SHELL` if usable, else `pwsh` | Shell to launch (then `powershell`, then `cmd`) | | `default-command` | Str | | Alias for default-shell | | `word-separators` | Str | ASCII punctuation except `_` | Characters that split words in copy mode, besides whitespace. The default is every printable ASCII character that is neither a letter nor a digit, minus the underscore, as in tmux | | `activity-action` | Str | `other` | Action on window activity: `any`, `none`, `current`, `other` | | `silence-action` | Str | `other` | Action on window silence: `any`, `none`, `current`, `other` | | `bell-action` | Str | `any` | Bell action: controls audible bell forwarding and status bar flag (`any`, `none`, `current`, `other`) | | `visual-bell` | Bool | `off` | Visual bell indicator | | `allow-passthrough` | Str | `off` | Allow terminal passthrough sequences (`on`/`off`/`all`) | | `extended-keys` | Choice | `off` | Whether a pane may ask for extended keys, as in tmux (`off`/`on`/`always`). Extended keys from the terminal are always decoded. With `on`, a program that writes `CSI > 4 ; 1 m` or `CSI > 4 ; 2 m` (Neovim does) gets modified keys such as `C-Space` in the extended form; `always` puts every pane in mode 1. `#{pane_key_mode}` shows `VT10x`, `Ext 1` or `Ext 2`. A pane that reads console records keeps win32 input mode records | | `extended-keys-format` | Choice | `xterm` | The format of extended keys written to a pane: `xterm` is `CSI 27;m;k~`, `csi-u` is `CSI k;mu` | | `allow-rename` | Bool | `on` | Allow programs to set window title via escape sequences | | `allow-set-title` | Bool | `off` | Allow programs to set pane title via OSC 0/2 escape sequences (see [pane-titles.md](pane-titles.md)) | | `allow-predictions` | Bool | `off` | Preserve PSReadLine prediction settings (see below) | | `default-terminal` | Str | | Terminal type string (sets `TERM` env var in panes) | | `update-environment` | Str | *(tmux defaults)* | Space-separated glob patterns; at new-session and on every attach each matching variable of the client's environment is copied into the session environment, and a pattern nothing matches is recorded as a `-NAME` removal (tmux environ_update) | | `warm` | Bool | `on` | Pre-spawn shells for instant window/pane creation (see [warm-sessions.md](warm-sessions.md)) | | `copy-command` | Str | | Shell command for clipboard pipe | | `codepoint-widths` | Str | | Comma separated overrides for how many columns Unicode codepoints occupy (see [Codepoint widths](#codepoint-widths)) | | `terminal-overrides` | Str | | Array of `pattern:cap:cap` entries matched against the client's `TERM`. `smcup@` and `rmcup@` keep the attached client off the host terminal's alternate screen (see [Terminal overrides](#terminal-overrides)) | | `set-clipboard` | Str | `external` | Clipboard interaction (`on`/`off`/`external`). A copy mode yank reaches the Windows clipboard on every value, with OSC 52 to the outer terminal unless `off`. An application's own OSC 52 creates a paste buffer only on `on`, as in tmux; on `external` psmux still forwards it to the outer terminal and the Windows clipboard, where tmux ignores it; `off` ignores it | | `main-pane-width` | String | `80` | Main pane width in the main-vertical layout, in cells or a percentage such as `30%` | | `main-pane-height` | String | `24` | Main pane height in the main-horizontal layout, in cells or a percentage such as `30%` | | `session-group` | Str | | Name of the session group this session joins. `none` or an empty value clears it. See [Session Groups](#session-groups) | | `command-alias` | Str | | Define your own command alias as `alias=expansion`. See [Command Aliases](#command-aliases) | ### Style Options | Option | Type | Default | Description | |--------|------|---------|-------------| | `status-left` | Str | `[#S] ` | Left status bar content | | `status-right` | Str | `#{?window_bigger,[#{window_offset_x}#,#{window_offset_y}] ,}"#{=21:pane_title}" %H:%M %d-%b-%y` | Right status bar content. This is **not** empty by default: out of the box it renders the pane title followed by the time and date | | `status-style` | Str | `bg=green,fg=black` | Status bar style | | `status-left-style` | Str | | Left status style | | `status-right-style` | Str | | Right status style | | `status-bg` | Str | | Legacy convenience setter. Rewrites only the `bg=` part of `status-style` and leaves the rest intact | | `status-fg` | Str | | Legacy convenience setter. Rewrites only the `fg=` part of `status-style` and leaves the rest intact | | `message-style` | Str | `bg=yellow,fg=black` | Message style | | `message-command-style` | Str | `bg=black,fg=yellow` | Command prompt style | | `mode-style` | Str | `bg=yellow,fg=black` | Copy-mode highlight | | `pane-border-style` | Str | | Inactive border style | | `pane-active-border-style` | Str | `fg=green` | Active border style | | `pane-border-hover-style` | Str | `fg=yellow` | Border style while the mouse hovers a draggable pane border | | `pane-border-indicators` | Str | `colour` | How the active pane is marked on its borders: `off` (no cue), `colour`, `arrows`, `both`. See [Pane Border Indicators](#pane-border-indicators) | | `pane-border-lines` | Str | `single` | Border glyph set: `single`, `double`, `heavy`, `simple`, `number`, `spaces`, `none`. See [Pane Border Lines](#pane-border-lines) | | `pane-border-format` | Str | | Pane border format string (e.g. `#{pane_index}: #{pane_title}`) | | `pane-border-status` | Str | | Pane border status position (`top`/`bottom`/`off`) | | `copy-mode-line-number-style` | Str | `fg=brightblack` | Style of the copy-mode line number gutter | | `copy-mode-current-line-number-style` | Str | `fg=yellow,bold` | Style of the line number on the copy-mode cursor row | | `copy-mode-match-style` | Str | `bg=cyan,fg=black` | Style of search matches in copy mode | | `copy-mode-current-match-style` | Str | `bg=magenta,fg=black` | Style of the search match under the copy-mode cursor | | `copy-mode-mark-style` | Str | `bg=red,fg=black` | Style of the line marked with `X` (set-mark); the marked cell has its colours swapped | | `window-style` | Str | | Style applied to the contents of panes that are not the active pane (tmux `options-table.c` wording), and the fallback for the active pane; fills only cells whose colour is the terminal default. Also accepts `dim=N` (0 to 100 percent) | | `window-active-style` | Str | | Style applied to the contents of the active pane; each of `fg` and `bg` falls back to `window-style` when this option does not name it. Explicit application colours always win. Also accepts `dim=N` | | `popup-border-style` | Str | `fg=yellow` | Border style of `display-popup` overlays. `display-popup -S` overrides its colours for one popup | | `popup-border-lines` | Str | `single` | Popup border glyph set: `single`, `double`, `heavy`, `simple`, `rounded`, `padded`, `none`. `display-popup -b` overrides it for one popup, `-B` draws no border | | `popup-style` | Str | | Colours of the popup content area: cells the program leaves at the terminal default take them. `display-popup -s` overrides its colours for one popup | | `clock-mode-colour` | Str | | Colour of the `clock-mode` digits | | `clock-mode-style` | Str | | Accepted and stored, but never read. See [Accepted but Not Functional](#accepted-but-not-functional) | | `window-status-format` | Str | `#I:#W#{?window_flags,#{window_flags}, }` | Inactive tab format. See [program-attention opt-in](program-status.md#opt-in-to-attention-indicators) | | `window-status-current-format` | Str | `#I:#W#{?window_flags,#{window_flags}, }` | Active tab format. | | `window-status-separator` | Str | `" "` | Tab separator | | `window-status-style` | Str | | Inactive tab style | | `window-status-current-style` | Str | | Active tab style | | `window-status-activity-style` | Str | `reverse` | Activity tab style | | `window-status-bell-style` | Str | `reverse` | Bell tab style | | `window-status-last-style` | Str | | Last-active tab style | ### Multi-line Status Bar (`status-format[]`) psmux supports a multi-line status bar using the `status-format[]` array. Set the `status` option to a number to control how many lines the status bar displays: ```tmux # Enable a 2-line status bar set -g status 2 # Configure each line (0-indexed) set -g status-format[0] "#[align=left]#S #[align=right]%H:%M" set -g status-format[1] "#[align=left]#{W:#[range=window|#{window_index}]#I:#W #[norange],#[range=window|#{window_index}]#I:#W* #[norange]}" ``` The first line (`status-format[0]`) replaces the default status bar content. Additional lines stack below (or above, depending on `status-position`). **Clickable window tabs.** In a custom `status-format[N]` line, tab click targets come from `#[range=window|N]`/`#[norange]` markers and nothing else: a window list written without them renders correctly but cannot be clicked, exactly as in tmux (tmux's shipped default `status-format[0]` carries these markers, which is why its window list is clickable out of the box). `N` is the window index itself, so `#[range=window|#{window_index}]` is the correct idiom at any `base-index`. Inside `#{W:inactive,current}` the comma separates the two loop arguments, so any other comma must be escaped as `#,` and combined styles are easier written as `#[fg=green]#[bold]` than `#[fg=green,bold]`. Ranges are honored on every status line, and clicks are hit-tested on every status row. ### Pane Border Labels Show pane information on the border between panes: ```tmux # Enable pane border labels at the top of each pane set -g pane-border-status top # Customize what the label shows set -g pane-border-format " #{pane_index}: #{pane_title} [#{pane_current_command}] " # Disable pane border labels set -g pane-border-status off ``` Use `select-pane -T "title"` to set a pane title that appears in the border label. Clear a title with `select-pane -T ""`. The default pane title is the hostname, matching tmux convention. > **Note:** PowerShell 7 automatically sets the pane title to the current working directory on every prompt via OSC escape sequences. If you see a file path in your pane border labels instead of the hostname, see [pane-titles.md](pane-titles.md) for details and options to control this. ### Pane Border Lines `pane-border-lines` picks the glyph set psmux draws pane borders with. After the borders are drawn, psmux runs a junction pass that upgrades straight runs into proper corner and tee glyphs, so `double` and `heavy` borders join cleanly instead of showing mismatched crossings. ```tmux # Default: thin single lines set -g pane-border-lines single # Double lines set -g pane-border-lines double # Thick lines set -g pane-border-lines heavy # ASCII only, for fonts without box drawing glyphs set -g pane-border-lines simple # Blank borders (the gap stays, the glyphs do not) set -g pane-border-lines spaces # No border glyphs at all set -g pane-border-lines none ``` `number` is accepted for tmux compatibility and renders the same as `single`. Any unrecognised value also falls back to `single`. ### Pane Border Indicators `pane-border-indicators` controls how the focused pane is marked on its borders. | Value | What the borders next to the active pane look like | | --- | --- | | `off` | No active-pane cue at all. Every separator keeps `pane-border-style`, and no arrows are drawn. | | `colour` (default) | A two-pane divider is split between `pane-border-style` and `pane-active-border-style`. With more than two panes the separators adjacent to the active pane use `pane-active-border-style`. | | `arrows` | The separators adjacent to the active pane use `pane-active-border-style`, plus inward-pointing markers on those separators and on the focused floating pane's border. No colour split. | | `both` | The `colour` split plus the `arrows` markers. | `pane-active-border-style` therefore applies in every mode except `off`, which is a genuine no-cue mode. When a floating pane owns focus, the tiled active cues are suppressed so the highlight follows the pane that actually has focus. ```tmux set -g pane-border-indicators arrows ``` `pane-border-indicators` is a global/default setting; per-window values are unsupported. ### Copy Mode Line Numbers Copy mode can draw a line number gutter down the left edge. It is off by default: ```tmux # Absolute line numbers counted from the top of the scrollback set -g copy-mode-line-numbers absolute # Distance from the cursor row, vi style set -g copy-mode-line-numbers relative # Cursor row shows its absolute number, every other row shows the relative distance set -g copy-mode-line-numbers hybrid # Distance from the current scroll offset set -g copy-mode-line-numbers default # Turn the gutter off again set -g copy-mode-line-numbers off ``` The gutter has two independent styles: ```tmux # Every line number except the cursor row set -g copy-mode-line-number-style "fg=brightblack" # The line number on the cursor row set -g copy-mode-current-line-number-style "fg=yellow,bold" ``` The gutter does not move what the mouse points at: a click or a drag in copy mode selects the cell under the pointer whichever mode is set. ### Cursor **With neither option set, which is how most configurations run, psmux asks the terminal for no cursor shape at all, so the cursor you had before starting psmux is the cursor you see inside it, blinking or steady as you had it.** A program running in a pane can still ask for its own shape, exactly as it would outside psmux, and psmux passes that through. This is what tmux does as well: its `cursor-style` defaults to `default`, and it sends a shape only once something has asked for one. When psmux does change the cursor, it puts it back on the way out, shape and blinking both, rather than resetting the terminal to its own default. It asks the terminal at attach what it is drawing (DECRQSS for the shape, DECRQM for DEC private mode 12) and writes that back when the session ends. A terminal that will not answer gets the plain reset instead, which is where tmux stops. `cursor-style` is the shape psmux asks for when you do want it to pick: ```tmux # Steady shapes set -g cursor-style block set -g cursor-style underline set -g cursor-style bar # The same three, blinking. tmux spells them this way and carries no separate # blink option, so a configuration written for tmux works unchanged set -g cursor-style blinking-block set -g cursor-style blinking-underline set -g cursor-style blinking-bar # Back to asking for nothing set -g cursor-style default ``` `cursor-blink` adds or removes the blink of whichever shape is named, so it is another way to spell the same six: ```tmux # A blinking bar, two ways set -g cursor-style blinking-bar set -g cursor-style bar set -g cursor-blink on # A steady bar, two ways set -g cursor-style bar set -g cursor-style blinking-bar set -g cursor-blink off ``` Set, `cursor-blink` decides; left alone, the shape's own name decides. It can also be set on its own, with no `cursor-style` at all: ```tmux # Blink the cursor the terminal already draws, whatever shape that is set -g cursor-blink on ``` A shape is asked for with DECSCUSR, which says whether it blinks, so there is nothing more to send for it. Without a shape there is no such code, and the blink goes out as DEC private mode 12 instead, which asks for the blink alone. Either way it is sent only because you asked: leave both options out and nothing is sent at all. That last form is the one terminals differ on. Windows Terminal acts on mode 12; WezTerm 20240203 records it, and reports it back when asked, but goes on drawing the cursor it was drawing. Both of them act on the blink a DECSCUSR code carries, so naming a shape is the form that works everywhere: `set -g cursor-style blinking-bar`, or `bar` with `cursor-blink on`. ### Popup and Window Styling `display-popup` overlays and the pane contents themselves can be styled separately from the borders around them: ```tmux # Popup border colour and glyph set set -g popup-border-style "fg=cyan" set -g popup-border-lines rounded # Background and foreground for every pane's contents set -g window-style "fg=colour247,bg=colour236" # ... and a brighter pair for the active pane, so focus is obvious set -g window-active-style "fg=colour250,bg=black" # Colour of the clock-mode digits (Prefix + t) set -g clock-mode-colour cyan ``` ### How the two window styles combine `window-active-style` does not replace `window-style` for the active pane, it layers on top of it one attribute at a time, matching tmux `tty.c` `tty_default_colours`. The active pane takes `fg` from `window-active-style` only when that option actually names an `fg`, and the same holds separately for `bg`. Anything it leaves unnamed comes from `window-style`. Three consequences worth knowing: * `set -g window-style "bg=colour52"` on its own tints **every** pane, the active one included. You do not need to repeat the colour in `window-active-style`. * Mixing works. With `window-style "bg=blue"` and `window-active-style "fg=red"`, the active pane is red on blue while the other panes keep the terminal foreground on blue. * `bg=default` (tmux colour 8) names no colour, so `window-active-style "bg=default"` reads as "inherit `window-style`", not as "use the terminal background". Both options also accept a `dim=N` percentage, where `N` runs from 0 to 100, as newer tmux does. It scales the pane's colours toward black by that percentage and applies to application colours too, not only to the ones the window style supplies. Unlike `fg` and `bg`, `dim` does not fall back: the active pane uses the `dim` from `window-active-style`, other panes use the one from `window-style`. ```tmux # Every pane sits on a dark red ground, the active pane simply undimmed set -g window-style "bg=colour52,dim=40" set -g window-active-style "bg=colour52" ``` `popup-border-lines` accepts the tmux set: `single` (the default), `double`, `heavy`, `simple` (`+`, `-` and `|`), `rounded`, `padded` (blank cells) and `none`. With `none`, or `display-popup -B`, the popup has no frame and its command gets the whole `-w` by `-h` area. As in tmux, only the colours of `popup-border-style` and `popup-style` are used, a popup has no title unless `display-popup -T` gives one, and the `-S` and `-s` flags of `display-popup` win over the options for that popup. > **Note:** `clock-mode-style` is accepted and stored but never read. See [Accepted but Not Functional](#accepted-but-not-functional). ### Bell When a program inside a pane emits BEL (`\x07`), psmux forwards the bell character to your host terminal so you hear the audible beep. The `bell-action` option controls when this happens and when the status bar tab gets a bell flag (`!`): ```tmux # Forward bell from any window (default) set -g bell-action any # Forward bell only from the active window set -g bell-action current # Forward bell only from non-active windows set -g bell-action other # Mute bell completely (no sound, no status bar flag) set -g bell-action none ``` The `window-status-bell-style` option controls how the tab looks when flagged: ```tmux set -g window-status-bell-style "fg=red,bold" ``` PowerShell example to test: ```powershell # These should all produce an audible beep inside psmux: Write-Host "`a" [Console]::Beep() [char]7 ``` ### Mouse Configuration Mouse support is enabled by default. You can customize how the mouse interacts with psmux: ```tmux # Disable mouse entirely (no click, scroll, or drag) set -g mouse off # Disable entering copy mode on mouse scroll set -g scroll-enter-copy-mode off # Enable tmux-like release-copy selection with pane clipping # Double-click selects a word, triple-click selects a line set -g pwsh-mouse-selection on ``` When `pwsh-mouse-selection` is `on`, releasing a left-drag copies the selected text immediately and clears the transient highlight. Right-click copy and `Ctrl+Shift+C` still work as explicit copy actions. When `scroll-enter-copy-mode` is `off`, scrolling in a pane does not enter copy mode and instead passes scroll events directly to the running application. A drag selection made over such a scrolled-back view still converts to a copy-mode selection when it reaches the pane's top or bottom row, and keeps auto-scrolling in that direction, so text taller than the window can be selected in one gesture (see [features.md](features.md)). #### Disabling psmux's drag selection (`mouse-selection`) Some TUI applications render their own internal layouts (multiple columns, sidebars, panels) inside a single psmux pane. Examples include `opencode`, `lazygit`, `nvim` with split windows, and similar dashboards. psmux's own client-side drag selection does not know about those internal layouts, so a left-click drag inside such an app draws a selection rectangle that crosses the app's internal columns instead of respecting them. If you would rather have the application handle mouse selection itself, disable psmux's drag selection: ```tmux # Let the app inside the pane handle its own mouse selection. # psmux will no longer render its drag-selection rectangle. set -g mouse-selection off ``` What still works when `mouse-selection` is `off`: - Click on a pane to focus it - Click on a window tab in the status bar to switch to it - Mouse wheel scrolling and scroll-into-copy-mode - Pane border drag-to-resize - Mouse events being forwarded to applications that request mouse tracking (DECSET 1000/1002/1003), so `opencode`, `htop`, `nvim`, `claude`, etc. continue to receive their clicks and drags What changes when `mouse-selection` is `off`: - psmux no longer draws its own selection rectangle on left-click drag - Right-click clipboard copy via psmux's selection is no longer triggered (selection never starts) - The `pwsh-mouse-selection` word/line multi-click and release-copy behavior is suppressed too while `mouse-selection off` is in effect To restore the default behaviour: ```tmux set -g mouse-selection on ``` You can also toggle this at runtime without restarting: ``` psmux set-option -g mouse-selection off psmux set-option -g mouse-selection on ``` This option is independent of `mouse` (which controls whether mouse events are received at all) and `pwsh-mouse-selection` (which only affects the style of the drag selection when it is active). ### Paste Detection (Ctrl+V Passthrough) On Windows, a terminal that pastes on Ctrl+V (Windows Terminal does by default) takes the key itself and hands psmux the clipboard as a burst of character events. psmux detects this pattern and reassembles it into a single bracketed paste for child applications. This is the `paste-detection` option and it is enabled by default. If the terminal does **not** paste on Ctrl+V (for example Windows Terminal with its `ctrl+v` paste action removed), the key reaches psmux as an ordinary key and is treated like one, as in tmux: a binding such as `bind -n C-v split-window -h` runs and pastes nothing, and with no binding the pane receives C-v, so **neovim** and **vim** get visual block mode. psmux waits a moment after the press to tell this apart from a terminal that delivers the press and then the pasted characters. If the terminal itself pastes on Ctrl+V, the key never reaches psmux or the application, and only the terminal's own setting can change that. To stop psmux from reassembling character bursts into bracketed pastes: ```tmux # Forward every key as typed, with no paste reassembly set -g paste-detection off ``` You can paste using: * **Ctrl+Shift+V** (Windows Terminal default paste shortcut) * **Right click** (paste in most terminals) * **Prefix + ]** (psmux paste from buffer) * **`psmux send-keys C-v`** from another terminal ### Live Preview in Choosers `choose-session` (prefix + s) and `choose-tree` (prefix + w) include a live preview pane that mirrors the selected session or window in real time. By default it is hidden and you press `p` to toggle it. To make it visible by default: ```tmux # Open all choosers with the preview pane already visible set -g choose-tree-preview on ``` You can still press `p` inside the chooser to hide it for the current session. The setting is read once when the chooser opens, so changes to the option take effect immediately on the next open. See [preview.md](preview.md) for the full feature documentation. ### Bold Is Bright (color rendering) Many terminals, including Windows Terminal, render bold text in one of the 16 basic ANSI colors as the brighter variant of that color. This is the common "bold is bright" behavior, and a bare shell gets it because it emits the standard SGR codes (`ESC[32m` for green, brightened by `ESC[1m`). psmux renders its screen through ratatui and crossterm, and crossterm serializes all 16 basic colors as the 256-indexed form (`ESC[38;5;N`) instead of the standard `30`-`37` codes. Windows Terminal only applies "bold is bright" to the standard codes, not the 256-indexed form, so colored bold text like PowerShell's `$PSStyle` output looked muted with a heavier font ([#425](https://github.com/psmux/psmux/issues/425)). psmux rewrites those basic-color sequences back to the standard codes so bold renders bright, exactly matching a bare shell. This is on by default. ```tmux # Default: basic colors get "bold is bright" (matches a bare shell) set -g bold-is-bright on # Opt out: pass crossterm output through untouched set -g bold-is-bright off ``` There is one tradeoff. crossterm collapses a basic color (`ESC[32m`) and an explicit 256-indexed low color (`ESC[38;5;2m`) into the identical bytes, so the rewrite cannot tell them apart and brightens both. If a program you use deliberately emits the 256-indexed colors 0 through 15 and you need them to stay exactly as sent, set `bold-is-bright off`. With it off, both basic and explicit 256-indexed low colors are byte-accurate, and you give up "bold is bright" on the basic colors. This is the inherent limitation of crossterm's lossy encoding; real tmux does not have it because it never collapses the two forms. The option applies from config, at runtime, and reports through `show-options` and `#{bold-is-bright}`: ```powershell psmux set-option -g bold-is-bright off psmux show-options -g bold-is-bright psmux display-message -p '#{bold-is-bright}' ``` ### Alternate Screen Full screen programs (`nvim`, `less`, `htop`) switch the terminal to the alternate screen with DEC private mode 47 or 1049, draw over it, and switch back on exit. That is why your scrollback is untouched after you quit them. psmux honours this by default. Setting `alternate-screen off` makes psmux ignore the switch, so everything those programs draw goes into the normal buffer and stays in the scrollback after they exit: ```tmux # Default: full screen apps get their own screen and leave no trace set -g alternate-screen on # Keep full screen output in the scrollback instead set -g alternate-screen off ``` The flag lives in each pane's terminal parser, and psmux patches live panes and the warm pane when you change it, so the new value applies immediately without restarting anything. ### Session Groups A session group is a name shared by several sessions. psmux exposes the membership through format variables so a status bar or a script can tell grouped sessions apart: ```tmux # Join this session to the "work" group set -g session-group work # Leave the group again set -g session-group none ``` The group can also be named when the server is spawned: ```powershell psmux server -g work ``` Grouped state is readable from any format string: ```powershell psmux display-message -p '#{session_group} #{session_grouped} #{session_group_size}' ``` ### Command Aliases `command-alias` defines your own short name for a command: ```tmux # "dev" now means "split-window -h" set -g command-alias 'dev=split-window -h' bind-key D dev ``` > **Note:** the alias is resolved by the server, so it works from a key binding, a config line, a hook, `run-shell` and the command prompt. It is **not** resolved by the command line front end, so `psmux dev` typed at a shell still fails with "unknown command". Wrap the CLI form in the real command name instead. ### Command Chaining psmux supports tmux-style command chaining with the `;` operator. Multiple commands on a single line are executed sequentially: ```tmux # Split and move focus in one binding bind-key M-s split-window -h \; select-pane -L # Create a development layout bind-key D split-window -v -p 30 \; split-window -h \; select-pane -t 0 ``` Each command ends at a newline or a `;`, in a config file as at the command prompt, so `set -g @a on ; set -g @b on` sets both. A `;` inside quotes, inside a `{ }` block or after a `#` that starts a comment is not a separator. Write `\;` where the semicolon has to stay inside a command's arguments, which is how a key binding holds a list of commands, as above. A plain `;` ends the `bind-key` itself: `bind-key x split-window ; select-pane -L` binds `split-window` and runs `select-pane -L` at once, as in tmux. ### Case-Sensitive Key Bindings psmux distinguishes between lowercase and uppercase letters in key bindings, matching tmux behavior: ```tmux # These are two different bindings: bind-key t clock-mode # Prefix + t (lowercase) bind-key T choose-tree # Prefix + Shift+T (uppercase) # Uppercase bindings for plugin managers bind-key I run-shell '~/.psmux/plugins/ppm/scripts/install_plugins.ps1' bind-key U run-shell '~/.psmux/plugins/ppm/scripts/update_plugins.ps1' ``` ### Ctrl+Space as Prefix Multi-character key names like `Space`, `Enter`, `Tab`, and `Escape` are fully supported in prefix configuration: ```tmux set -g prefix C-Space unbind-key C-b bind-key C-Space send-prefix ``` ### psmux Extensions (Windows-specific) | Option | Type | Default | Description | |--------|------|---------|-------------| | `prediction-dimming` | Bool | `off` | Dim predictive/speculative text | | `cursor-style` | Str | `default` | The cursor shape psmux asks the terminal for. `block`, `underline` and `bar` are steady; `blinking-block`, `blinking-underline` and `blinking-bar` blink; `default` asks for no shape at all. See [Cursor](#cursor) | | `cursor-blink` | Bool | `off` | Adds or removes the blink of the shape above: `on` makes a steady shape blink, `off` stops a blinking one. Set on its own, with no shape named, it asks the terminal for the blink alone and leaves the shape as it was. See [Cursor](#cursor) | | `env-shim` | Bool | `on` | Inject Unix-compatible `env` function in PowerShell panes | | `claude-code-fix-tty` | Bool | `on` | Patch Node.js process.stdout.isTTY for Claude Code | | `claude-code-force-interactive` | Bool | `on` | Set CLAUDE_CODE_FORCE_INTERACTIVE=1 in panes | | `@heal-crashed-panes` | Bool | `off` | Respawn a pane whose shell exits within a short grace window of being spawned, instead of closing the window. Only needed where PowerShell's non-PSReadLine fallback reader dies on its first ConPTY read | `bold-is-bright` and `paste-detection` are psmux extensions too, but they are defined once in [All Set Options](#all-set-options) rather than repeated here. Their behaviour is explained in [Bold Is Bright](#bold-is-bright-color-rendering) and [Paste Detection](#paste-detection-ctrlv-passthrough). ### Process Priority Windows gives its foreground scheduling boost to whichever process owns the foreground window. The psmux server owns no window at all, and the attach client draws inside a console window that the terminal host owns, so neither of them ever receives that boost. On an idle or lightly loaded machine this costs nothing. On a heavily oversubscribed one it means your keystrokes queue behind every compute job on the box, and typing starts to lag. `priority` sets the scheduling class of psmux's **own** processes, and nothing else: ```tmux # Default: a small boost, enough to stay ahead of background compute set -g priority above-normal # Turn the boost off entirely set -g priority normal # For a machine you deliberately oversubscribe. Use sparingly set -g priority high ``` Any other value is refused: `set -g priority realtime` exits nonzero with a message and leaves the class where it was. Realtime is not offered at all, because a process at that class outranks most of the kernel's own threads. **What it applies to, exactly:** | | | |---|---| | The server process | Immediately, the moment the option is set, and again at every server startup | | The attach client | At client startup, from `PSMUX_PRIORITY` or from a `priority` line in your config file | | A warm standby server | At its own startup, so a session that claims one gets the configured class | | Pane shells and your programs | **Never.** A Windows child created with no explicit class flag gets `NORMAL_PRIORITY_CLASS` regardless of its parent, so nothing you run inside a pane is raised | Two things it does not do. A `set -g priority` issued in one session does not reach a warm standby that is already parked in the pool, and it does not retroactively change a client that is already attached; both pick the value up the next time they start. And an already running client keeps its class for its whole life, so use `PSMUX_PRIORITY` or a config file line if you want every client to agree. `PSMUX_PRIORITY` overrides the option, for both processes. That ordering is deliberate: it is the escape hatch that lets you climb out of a configured value from the shell you start psmux in, without editing a file. `show-options -g priority` always reports what the process is **actually** running at, so if the environment won you will see the environment's value there. ```powershell # This shell only $env:PSMUX_PRIORITY = "normal" psmux ``` Setting the class can be refused by a restricted token or by a job object that caps it. psmux treats that as nothing to report: it carries on at whatever class it already had rather than failing to start. For reference, tmux has no equivalent option. It never sets a scheduling priority anywhere in its source, because the Linux scheduler already favours a process that spends its life blocked on a read. This is a Windows specific extension rather than a parity feature. ### Style Value Grammar Every `*-style` option and every inline `#[...]` block in a format string uses the same comma separated grammar: ```tmux set -g status-style "fg=white,bg=colour236,bold" set -g status-left "#[fg=green,bold]#S#[default] " ``` **Colours.** `default` and `terminal` (both mean the terminal's own default), `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, the bright variants (`brightblack` through `brightwhite`, also spelled `bright-black`), `colour0` to `colour255` (the American spelling `color0` works too), `#RRGGBB`, `idx:N`, and `rgb:R,G,B`. **Attributes.** `bold`, `dim`, `italics` (also `italic`), `underscore` (also `underline`), `double-underscore`, `curly-underscore`, `dotted-underscore`, `dashed-underscore`, `blink`, `reverse`, `hidden`, `strikethrough`. **Underscore colour.** `us=colour` sets the colour of the underline on its own, separate from `fg=`: ```tmux set -g message-style "curly-underscore,us=red" ``` The styled underscores and `us=` need a terminal that understands the SGR 4 subparameter forms (`4:2` through `4:5`) and SGR 58. Windows Terminal, WezTerm and kitty all do; a terminal that does not falls back to a plain underline. **Negations.** Each attribute has a matching keyword that removes it again: `nobold`, `nodim`, `noitalics`, `nounderscore` (also `nounderline`, and it clears the styled underscores too), `noblink`, `noreverse`, `nohidden`, `nostrikethrough`. These matter inside a format string, where a later `#[...]` block builds on the style already in effect rather than starting from nothing: ```tmux # Bold for the session name, then drop bold only, keeping the colour set -g status-left "#[fg=cyan,bold]#S#[nobold] #{pane_current_command}" ``` **Reset.** `default` or `none` returns to the base style of whatever is being drawn. **Style stack.** `push-default` saves the style in effect, and `pop-default` restores the most recently saved one. Use them to make a temporary change without having to spell out how to undo it: ```tmux # Save the bar style, highlight the window list, then restore exactly what was there set -g status-left "#[push-default]#[fg=black,bg=yellow] #S #[pop-default] ready" ``` A `pop-default` with nothing on the stack falls back to the base style, so an unbalanced format degrades instead of breaking. `fill`, `align=...`, `list=...`, `nolist`, `range=...` and `norange` are accepted for tmux compatibility and are handled by the status bar layout code rather than by the style parser. ## Configuration File Conditionals Config files support tmux's `%if` directives, so one file can serve several machines. Conditions are ordinary format strings, which means anything you can write inside `#{...}` can drive a branch. A condition is true when it expands to something that is neither empty nor `0`. ```tmux # A hidden variable. Later config lines can reference it as $THEME or ${THEME}, # and format strings (including %if conditions) can read it as #{THEME}. %hidden THEME=dark %hidden ACCENT=cyan %if "#{==:#{THEME},dark}" set -g status-style "bg=colour236,fg=colour250" set -g pane-active-border-style "fg=$ACCENT" %elif "#{==:#{THEME},light}" set -g status-style "bg=colour253,fg=black" set -g pane-active-border-style "fg=blue" %else set -g status-style "bg=green,fg=black" %endif # Conditions can read your own user options, and blocks nest set -g @work-machine yes %if "#{==:#{@work-machine},yes}" set -g status-right "#{@work-machine} %H:%M" %if "#{mouse}" set -g pwsh-mouse-selection on %endif %endif ``` The directives: | Directive | Effect | |---|---| | `%if ` | Open a block. Lines inside it run only when the condition is true | | `%elif ` | Try another condition, but only if no earlier branch in this block matched | | `%else` | Run when no earlier branch matched | | `%endif` | Close the innermost block | | `%hidden NAME=value` | Define `NAME` for the rest of the config, and for panes spawned from this session | Notes: - Blocks nest. An inner `%if` inside a branch that was skipped is skipped as a whole, so you never get a partly applied inner block. - The condition may be quoted with single or double quotes; the quotes are stripped before it is expanded. - Only the first matching branch runs. Once one has, later `%elif` and `%else` branches are skipped. - `%hidden` assignments inside a skipped branch are not applied. - The two ways to read a `%hidden` variable are not interchangeable. `$NAME` and `${NAME}` are substituted only on ordinary config lines, so they do **not** work inside a `%if` condition. Use the format form `#{NAME}` there, which resolves through the same session environment. - `%hidden` writes a hidden entry into the global environment, as tmux does: it is usable as `$NAME` and `#{NAME}`, it is listed only by `show-environment -gh`, and it is never passed to new panes. ## Accepted but Not Functional These options parse cleanly, survive `show-options`, and do absolutely nothing. They exist so that an imported `.tmux.conf` loads without errors. They are listed here so you do not spend time debugging a setting that was never wired up. | Option | Status | |---|---| | `lock-after-time` | Accepted and stored. Session locking is not implemented, so the timer never runs. `lock-client`, `lock-server` and `lock-session` exist as commands but nothing locks on a timer | | `lock-command` | Accepted and stored. Never read, for the same reason | | `clock-mode-style` | Accepted and stored. Use `clock-mode-colour`, which is read | Setting any of these produces no warning, because the values are stored verbatim in the user options map instead of being validated against a known option. For most of them `show-options` will read the value back to you unchanged, and that is the trap: a value that round-trips is not evidence that it took effect. ## Environment Variables Most of psmux is configured with options, not environment variables. The variables below exist either because the setting has to be known before a config file is read, or because it is a per shell escape hatch you want for one invocation rather than forever. ### Startup and session selection | Variable | Effect | |---|---| | `PSMUX_CONFIG_FILE` | Replaces the config file search entirely. Set for you by `psmux -f `. A leading `~` is expanded | | `PSMUX_DATA_DIR` | Absolute path of the directory that holds the server registry (`.port`, `.key`, `.sid`, `.pid` files), logs and other runtime state, instead of `~\.psmux`. Two data directories are fully independent: each has its own single server guard, so both can hold a session of the same name, including the `__warm__` standby. Must be absolute and non empty | | `PSMUX_DEFAULT_SESSION` | Session name used when nothing else determines one | | `PSMUX_SESSION_NAME` | Target session for a bare `psmux` or a control mode invocation. psmux also sets this itself when it spawns | | `PSMUX_TARGET_SESSION` | Session that CLI commands address when you give no `-t`. Exported into panes, which is how a command run inside a pane knows where it is | | `PSMUX_TARGET_FULL` | The full `session:window.pane` target string for CLI commands. Set by the global `-t` parse | | `PSMUX_ALLOW_NESTING` | Set to `1` to permit running psmux inside a psmux pane. Without it the nesting guard refuses, because a nested client would fight the outer one for the console | | `PSMUX_REMOTE_ATTACH` | Marks the invocation as a remote attach, which skips the bare invocation session bootstrap | | `PSMUX_ACTIVE` | Set to `1` on a client process to mark that it owns the console. This is what the nesting guard reads | | `PSMUX_SWITCH_TO` | Handshake variable carrying the session name across a `switch-client` | | `PSMUX_CLIENT_LAST_SESSION` | Handshake variable set by `switch-client` with the session the client is leaving, so `switch-client -l` returns to a session this client actually visited | | `PSMUX_SESSION_DISPLAY_NAME` | The session name as you typed it, kept alongside the on disk `__` spelling that `-L` produces, so error messages show the name you used | | `PSMUX_NO_WARM` | Set to `1` to disable warm pane and warm server pre-spawning. Equivalent to `set -g warm off`. See [warm-sessions.md](warm-sessions.md) | | `PSMUX_PRIORITY` | Scheduling class for the psmux server and client processes: `normal`, `above-normal` or `high`. Overrides the `priority` option, so it is the per shell way out of a configured value. An unrecognised value is reported and ignored. See [Process Priority](#process-priority) | ### Appearance and rendering | Variable | Effect | |---|---| | `PSMUX_CURSOR_STYLE` | Cursor shape: `bar`, `block`, `underline`, or `default`. Normally set for you by `set -g cursor-style` | | `PSMUX_CURSOR_BLINK` | Cursor blink. `1`, `on` or `true` enables it, anything else disables it, and leaving it unset is not the same as `0`: see `cursor-blink` above. Normally set for you by `set -g cursor-blink` | | `PSMUX_DIM_PREDICTIONS` | Dim PSReadLine prediction text for this shell only. The option form is `prediction-dimming` | | `PSMUX_HOST_COLORS` | Supplies the host terminal's palette so psmux can answer OSC 4, 10 and 11 colour queries. psmux normally queries the host itself; set this when the host misreports or when the query cannot run | | `PSMUX_XTVERSION_NAME` | The terminal name psmux reports when a pane asks XTVERSION (`ESC [ > q`). Defaults to `tmux`, matching the identity a pane already sees in `$TMUX` and in the first line of `psmux -V`. Set it to `psmux` to be announced under psmux's own name | ### Windows and transport escape hatches | Variable | Effect | |---|---| | `PSMUX_NO_PASSTHROUGH` | Set to `1` to disable the experimental ConPTY passthrough flag on Windows build 22621 and newer. Use this if pane creation fails with `ERROR_INVALID_PARAMETER` | | `PSMUX_CONPTY_DIR` | Point at a directory holding a `conpty.dll` and the `OpenConsole.exe` beside it, and every pane is hosted by that console host instead of the inbox `conhost.exe`. psmux ships neither file and loads nothing unless you name the directory. A directory that does not exist, or a DLL that will not load, falls back to the system ConPTY with a logged reason. This is an escape hatch for a host whose inbox console has a defect you cannot otherwise get around, and it has costs of its own: see the console host seam in `docs/diagnostics.md` for the measurements. The host is chosen once per server, so a new session takes the value from the shell that creates it: a standby server started with a different value is not claimed, and the session starts cold instead | | `PSMUX_PIPE_VT` | Forces pipe mode VT handling for Cygwin and MSYS style PTYs. `1` forces it on, `0` forces it off. Left unset, psmux detects the pipe itself | | `PSMUX_BARE_ENV` | Spawn panes with a bare environment instead of inheriting yours. Useful when a broken inherited variable stops shells from starting | | `PSMUX_FORCE_MOUSE` | Overrides the ConPTY mouse safety gate. On Windows builds below 22523 psmux refuses to enable mouse reporting, because on Windows 10 era conhost the first click could fast fail the console host and take the pane down with it. Some later builds under that threshold, Windows Server 2022 (20348) among them, handle mouse perfectly well but still need psmux to write the enable sequence itself. Set to `1` there to get the mouse back. Set to `0` to force it off on a newer build whose console host misbehaves. Accepts `1`, `on`, `true`, `yes` and their negatives. If your session dies the moment you click, unset it | | `PSMUX_FORCE_WHEEL` | Set to `1` to forward the wheel into every pane on this server regardless of whether its application registered for the mouse. This is the server wide last resort; prefer the pane scoped `set-option -p -t %N @mouse-force on`, which is the scope the decision belongs at. psmux normally forwards only to a pane that asked, and it now keeps that authorization for as long as the process that earned it is alive, so a `node` child entering raw mode no longer takes it away. Reach for this only when an application registers through neither channel psmux can see and the wheel is dead in the pane from its first instant. Leave it unset if you run programs that do not read the mouse: they receive the report as literal keystrokes, which is what the gate exists to prevent. Accepts `1`, `on`, `true`, `yes`; anything else keeps the gate | ### Set inside panes by psmux These are exported into every pane, so scripts can detect that they are running under psmux and where: | Variable | Effect | |---|---| | `TMUX` | Socket path and server info, for tmux compatibility. This is what most tools check | | `TMUX_PANE` | Current pane id (`%0`, `%1`, and so on) | | `PSMUX_SESSION` | Current session name | | `PSMUX_POPUP` | Set to `1` on the child of a `display-popup`, so the nesting guard does not mistake it for a pane and a popup can run `psmux` commands | Setting a variable for one shell, or for good: ```powershell # This shell only $env:PSMUX_NO_WARM = "1" psmux # Every future shell setx PSMUX_NO_WARM 1 ``` > **Note:** psmux also has a set of `PSMUX_*_DEBUG` logging variables. They are documented in > [diagnostics.md](diagnostics.md) rather than here, because they are for reporting a problem rather > than for configuring psmux. ## Managing Environment Variables Use `set-environment` to set env vars that are inherited by newly created panes: ```powershell # Set a global env var (inherited by all new panes) psmux set-environment -g EDITOR vim # Set a session-scoped env var psmux set-environment MY_VAR value # Unset an env var psmux set-environment -gu MY_VAR # Keep a variable OUT of new panes, even one the server inherited psmux set-environment -r MY_VAR # Show the session environment, the global one, or one variable psmux show-environment psmux show-environment -g psmux show-environment MY_VAR # MY_VAR=value, -MY_VAR, or "unknown variable: MY_VAR" (exit 1) psmux show-environment -s # shell syntax: NAME="value"; export NAME; ``` As in tmux there are two scopes. The global environment (`-g`) is the server's start environment (the environment of the shell that started the session) plus anything set with `-g`; the session environment holds what was set without `-g`, and `new-session -e`. A new pane gets the global environment with the session one laid over it, without names removed by `-r` or marked hidden with `-h`. Because psmux runs one server per session, "global" means global to that session's server, so `set-environment -g` affects the session it is sent to rather than every session. Environment variables set this way are injected at the process level when new panes spawn, so they are completely invisible (no commands echoed in the shell). ## PSReadLine Predictions (Intellisense / Autocompletion) By default, psmux disables PSReadLine inline predictions (the grayed-out autocompletion/intellisense suggestions that appear as you type) to avoid additional unexpected bugs caused by the interaction between predictions and ConPTY. This means `PredictionSource` defaults to `None` inside psmux, even if your profile sets it to `HistoryAndPlugin` ([#150](https://github.com/psmux/psmux/issues/150)). If enough people test predictions and the community supports enabling them by default, this will be changed in a future release. To preserve your prediction/autocompletion settings, enable `allow-predictions`: ```tmux set -g allow-predictions on ``` With this enabled: - If your profile sets `PredictionSource`, psmux respects your choice - If your profile does not set it, psmux restores the system default (typically `HistoryAndPlugin`) ## Prediction Dimming Prediction dimming is off by default. If you want psmux to dim predictive/speculative text (e.g. shell autosuggestions), you can enable it in `~/.psmux.conf`: ```tmux set -g prediction-dimming on ``` You can also enable it for the current shell only: ```powershell $env:PSMUX_DIM_PREDICTIONS = "1" psmux ``` To make it persistent for new shells: ```powershell setx PSMUX_DIM_PREDICTIONS 1 ``` ## Reloading Configuration at Runtime You can reload your config file without restarting psmux. From the command prompt (`Prefix + :`), run: ```tmux source-file ~/.psmux.conf ``` Or from outside psmux: ```powershell psmux source-file ~/.psmux.conf ``` This re-executes every line in the config file, applying any changes to options, key bindings, hooks, and styles immediately. ### What source-file does to key bindings Like tmux, `source-file` only runs the file's commands: bindings made before it stay. A `bind` typed at the CLI survives, and files sourced one after another (a plugin manager's, for example) all keep their bindings. The file's commands run in order, each once the one before it has finished, as tmux runs them: a `split-window` after a `new-window` splits the new window. The options and bindings before the file's first command apply at once; everything from that command on waits its turn. `psmux source-file` returns once the commands have run and prints what they print. Every problem in the file is an error, as in tmux: a command that fails, a nested file that cannot be read, an unknown option or a bad value. `source-file` prints each on stderr and exits 1, and the rest of the file still runs. With `-q`, a file that does not exist is skipped without a word. `source-file` takes one or more paths, as tmux does, and reads them in the order given: `source-file ~/.psmux.conf ~/.psmux.local.conf`. Quote a path that has a space in it. A path that cannot be read is reported and the others are still read. An older line that names one file with spaces and no quotes, such as `source-file C:\Users\Ann Lee\x.conf`, still reads that file. A file may source other files, and a path may be a glob. A nested file's commands run where its `source-file` line stands, before the rest of the outer file, down to 50 levels as in tmux (deeper is `too many nested files`). A file that would source a file already being sourced, itself or one further out, is an error. When the `source-file` line always runs, `source-file` refuses the whole file before any of its lines take effect; one in a key binding, a `%if` block or an `if-shell` block is checked only if and when it runs, so a reload key such as `bind r { source-file ~/.tmux.conf }` is fine. tmux has no such check and runs a self-sourcing file until the nesting limit. One deliberate difference from tmux: sourcing the config file the server **started with** (the `-f` file or `PSMUX_CONFIG_FILE`, otherwise the first of `~/.psmux.conf`, `~/.psmuxrc`, `~/.tmux.conf`, `~/.config/psmux/psmux.conf` that existed) is a reload, and it starts from the default bindings. The reason is [#195](https://github.com/psmux/psmux/issues/195): a config that stops saying `unbind-key -a` gets the default bindings back when it is reloaded. tmux keeps them unbound until the server restarts. The comparison ignores case and slash direction, so `source-file ~/.PSMUX.conf` is the same reload. Because the reload starts from the defaults, bindings made outside the config (at the CLI, or by a file the config does not source) are dropped by it; re-source those files from the config if you want them to survive a reload. ## Window and Pane Numbering By default, windows and panes are numbered starting from 0. You can change the starting index for both: ```tmux # Start window numbering at 1 set -g base-index 1 # Start pane numbering at 1 set -g pane-base-index 1 ``` The `pane-base-index` setting affects: - **Display Panes overlay** (`Prefix + q`): The numbers shown on each pane start from your configured base index - **Pane targets**: When referencing panes by number (e.g. `select-pane -t 1`), numbering follows your base index - **Format variables**: `#{pane_index}` reflects the base index setting - **Status bar and border labels**: Pane numbers in format strings use the configured base A common setup for both windows and panes to start at 1: ```tmux set -g base-index 1 set -g pane-base-index 1 ``` ## Display Panes Overlay Press `Prefix + q` to show numbered overlays on each pane. While the overlay is visible, press any displayed number key to jump to that pane. The overlay auto-dismisses after `display-panes-time` milliseconds (default: 1000ms). ```tmux # Show pane numbers for 3 seconds set -g display-panes-time 3000 ``` The numbers shown respect your `pane-base-index` setting. For example, with `pane-base-index 1`, three panes show as 1, 2, 3 instead of 0, 1, 2. You can also trigger this overlay from the command line: ```powershell psmux display-panes ``` ## Split Window Options When splitting panes, you can control the size and starting directory of the new pane: ```tmux # Split vertically, new pane takes 30% of the space split-window -v -p 30 # Split horizontally, new pane takes 70% of the space split-window -h -p 70 # Split and start in a specific directory split-window -v -c "C:\Projects\myapp" # Split and start in the current pane's directory split-window -h -c "#{pane_current_path}" # Split and run a specific command split-window -v -- python ``` These flags also work when creating new windows: ```tmux # New window with a specific name new-window -n "logs" # New window in a specific directory new-window -c "C:\Projects" # New window running a specific command with a name new-window -n "build" -- cargo build --watch ``` When you set a window name with `-n`, the `automatic-rename` flag is turned off for that window so psmux does not overwrite your chosen name with the foreground process name. To re-enable automatic renaming for that window: ```tmux set-option -w automatic-rename on ``` ## Detach and Exit Policies Control what happens when clients disconnect or all windows close: ```tmux # Exit the server when no clients are attached (default: off) set -g destroy-unattached on # Exit the server when the last window/session closes (default: on) set -g exit-empty on ``` With `destroy-unattached on`, the server process terminates as soon as the last client detaches. This is useful for single-use sessions. With `exit-empty off`, an empty server stays alive. The tmux idiom for holding a private server before it has a session works as it does in tmux: ```powershell psmux -S $sock start-server ';' set-option -g exit-empty off psmux -S $sock display-message -p "#{socket_path}`t#{pid}" # answers before any session exists psmux -S $sock new-session -d -s work # lands in that same server process ``` psmux keeps one process per session, so its empty server is the namespace's standby server, made visible while it is unclaimed with `exit-empty off`. The first `new-session` in that namespace becomes that process, whatever flags it carries (`-f` is ignored, as tmux ignores it for a running server). With `exit-empty on` (the default), a plain `start-server` leaves nothing to query, as in tmux, where the server exits at once. Unlike tmux, killing the last session of a namespace still ends that session's process even with `exit-empty off`. ## Dead Panes and Respawn When a process inside a pane exits, the pane normally closes. To keep the pane visible after its process exits: ```tmux set -g remain-on-exit on ``` A pane with a dead process shows its last output and can be respawned: ```powershell # Restart the default shell in the pane psmux respawn-pane # Kill any remaining process and restart psmux respawn-pane -k # Respawn in a different directory psmux respawn-pane -c "C:\Projects" # Respawn with a specific command psmux respawn-pane -- python app.py # Give the new process extra environment (repeatable, this process only) psmux respawn-pane -k -e AGENT_ID=bob -e ROLE=review -- claude ``` This is useful for monitoring: if a long-running process crashes, you can see its final output and restart it without losing the pane layout. A respawn behaves like tmux's: the pane keeps its id and its scrollback history, so the new process starts below everything its predecessor printed (`capture-pane -S -` and copy mode still reach it). The rows that were on screen when the old process died are cleared rather than moved into history, the cursor goes back to the top left, copy mode is left, and a process that died on the alternate screen leaves the pane on the normal one. Use `clear-history` after the respawn when you want a clean slate. ### Background Processes and `@kill-descendants` On Unix, tmux relies on the kernel's SIGHUP delivery when a pane's terminal closes, so a process that was deliberately detached (for example with `nohup`) survives its pane. Windows has no SIGHUP and no pty process groups, so psmux instead walks the pane's process tree. By default, when a pane's shell exits on its own, psmux terminates any child processes the shell left behind (for example something launched with `Start-Process`), because otherwise those processes and their `conhost.exe` hosts accumulate invisibly and can exhaust the desktop heap. If you intentionally launch background processes from a pane and want them to outlive the shell, opt out (psmux extension): ```tmux # Let background children survive when their pane's shell exits on its own set -g @kill-descendants off ``` Notes: - This only affects panes whose shell exits on its own. Explicit `kill-pane`, `kill-window`, and `kill-session` always terminate the pane's full process tree. - With `remain-on-exit on` the pane is kept instead of pruned, so no sweep happens either way until the pane is actually closed. - Recognized off values: `off`, `0`, `false`, `no`. Anything else, including unset, keeps the sweep enabled. ## Session Environment Variables You can set environment variables at the session or global level that get inherited by all new panes: ```powershell # Set a global env var (all new panes in all sessions inherit this) psmux set-environment -g EDITOR vim # Set a session-scoped env var psmux set-environment MY_VAR value # Unset a global env var psmux set-environment -gu MY_VAR # View all environment variables psmux show-environment psmux show-environment -g ``` You can also pass environment variables when creating a new session: ```powershell # Create a session with custom environment psmux new-session -s work -e "PROJECT=myapp" -e "ENV=production" ``` ## Status Bar Time Updates The status bar supports time format variables that update in real time: ```tmux # Show current time in the status bar (updates every second) set -g status-right "%H:%M:%S %d-%b-%y" # Common time format variables: # %H Hour (24-hour, 00-23) # %I Hour (12-hour, 01-12) # %M Minute (00-59) # %S Second (00-59) # %p AM/PM # %r Full time in 12-hour format (e.g. 02:30:45 PM) # %R Hour:Minute in 24-hour format (e.g. 14:30) # %d Day of month (01-31) # %b Abbreviated month name (Jan, Feb, ...) # %Y Full year (2025) # %a Abbreviated weekday (Mon, Tue, ...) ``` Time variables refresh based on the `status-interval` option (default: 15 seconds). For second-level precision, reduce the interval: ```tmux # Update status bar every second (for live clock) set -g status-interval 1 ``` ## PSReadLine ListView psmux supports PSReadLine's ListView prediction style, which shows a dropdown list of suggestions: ```powershell # In your PowerShell profile ($PROFILE) Set-PSReadLineOption -PredictionSource HistoryAndPlugin Set-PSReadLineOption -PredictionViewStyle ListView ``` For this to work inside psmux, enable `allow-predictions` in your psmux config: ```tmux set -g allow-predictions on ``` Without `allow-predictions on`, psmux resets PSReadLine's prediction settings during initialization, which disables ListView mode. ## Codepoint widths Some characters have no single correct display width. Unicode marks a large group, including box drawing characters, many symbols, and the characters `tig` draws its commit graph with, as **East Asian Ambiguous**: one column in a Western context, two in a CJK one. psmux resolves ambiguous characters to **one** column, which is what tmux does. If your terminal draws them as two, the two of you disagree about where every following cell on the line begins, and characters can be left painted on screen after the text around them is erased. `codepoint-widths` lets you settle the disagreement. It is a server option, so it is written with `set -s`, and it takes a comma separated list of overrides: ```tmux # One codepoint, written in hex set -s codepoint-widths "U+2502=2" # A range, inclusive at both ends (note the U+ on BOTH sides) set -s codepoint-widths "U+2500-U+257F=2" # A literal character works too set -s codepoint-widths "|=2" # Several entries at once set -s codepoint-widths "U+2500-U+257F=2,U+25CF=2" ``` The width must be `0`, `1` or `2`. An entry that is malformed, out of range, or names a codepoint that does not exist is ignored, and the remaining entries still apply. Because it is an array option, `-a` appends rather than replaces: ```tmux set -s codepoint-widths "U+2500-U+257F=2" set -sa codepoint-widths "U+25CF=2" # both entries now apply set -su codepoint-widths # back to the default (no overrides) ``` A change takes effect immediately, for text drawn after it. Text already on screen keeps the width it was drawn with, so redraw the pane or restart the program to see an override applied to content that is already there. Read the current value back with: ```powershell psmux show-options -s codepoint-widths ``` If you are unsure whether your terminal treats a character as one column or two, print a row of them and see where it wraps: in an 80 column window, 80 of them filling exactly one line means one column each, and wrapping after 40 means two. ## Terminal overrides `terminal-overrides` is the tmux option for adjusting what psmux assumes about the terminal a client is attached from. psmux has no terminfo database: the client writes VT sequences directly. So of all the capabilities tmux knows, psmux honours the two that change what the client sends on its own account: | Capability | Effect | |---|---| | `smcup` | Entering the host terminal's alternate screen (`ESC[?1049h`) when a client attaches | | `rmcup` | Leaving it again (`ESC[?1049l`) when the client detaches or exits | Every other capability is accepted, kept, shown by `show-options`, and ignored. The common use is keeping the client on the host terminal's main screen, for example an SSH client on a phone where the alternate screen cannot be scrolled: ```tmux set -ga terminal-overrides ',*:smcup@:rmcup@' ``` With that set the client never sends `ESC[?1049h` or `ESC[?1049l`. Like tmux, it clears the screen when it starts drawing and again when it detaches, so the prompt comes back on a clean screen. psmux redraws the screen in place, so pane output does not pile up in the host terminal's scrollback; use copy mode for a pane's history. How entries are read, the same way tmux reads them: - It is an array option. Each element is `pattern:cap:cap...`. `set -g` replaces the whole array, `set -ga` adds elements (a leading comma is optional), and `set -gu` empties it. - The pattern is matched against the `TERM` of the attaching client with shell style wildcards (`*`, `?`, `[...]`). Elements are applied in order, so a later element wins over an earlier one. - `cap@` removes a capability, `cap=value` sets it, and `::` is a literal colon inside a field. A capability set to an empty value counts as removed. - Native Windows consoles usually have no `TERM` at all. An unset `TERM` is matched as the empty string, which is what tmux's own client sends, so `*` applies to it and a pattern such as `xterm*` does not. The option is read by the server, from the config file or from a `set` at runtime, and each client decides when it attaches. A change made while a client is attached applies from the next attach. Read it back with: ```powershell psmux show-options -g terminal-overrides # terminal-overrides[0] *:smcup@:rmcup@ ``` --- # Key Bindings Default prefix: `Ctrl+b` (same as tmux). Change with `set -g prefix C-a`. Supported prefix keys include `C-a` through `C-z`, `C-Space`, and any printable character. Changing the prefix with `set -g prefix ` also auto binds the new key to `send-prefix`, so pressing the prefix twice always forwards one literal prefix byte to the active pane. That is how a shell like bash or nushell still sees `Ctrl+a` as "go to start of line" when `C-a` is your prefix. A second prefix is available with `set -g prefix2 `. Both keys are checked on every keystroke, so either one arms the prefix table. ## Case Sensitivity Key bindings are **case-sensitive**, matching tmux behavior: - `bind-key t` binds to lowercase `t` (just press `t`) - `bind-key T` binds to uppercase `T` (`Shift+t`) This is essential for plugins like PPM (`Prefix+I`/`Prefix+U`) and psmux-sensible (`Prefix+R`). ## Key Tables A key table is a named set of bindings. psmux consults these tables: | Table | How keys reach it | Default contents | |-----|--------|--------| | `root` | Every keystroke, before the prefix is armed. Bind with `bind-key -n ` or `bind-key -T root ` | **Empty.** psmux binds nothing here by default | | `prefix` | Keystrokes that follow the prefix key. Bind with plain `bind-key ` | The full default set listed below | | `copy-mode` | Keystrokes while copy mode is active and `mode-keys` is `emacs` (the default) | Empty. Bind with `bind-key -T copy-mode ` | | `copy-mode-vi` | Keystrokes while copy mode is active and `mode-keys` is `vi` | Empty. Bind with `bind-key -T copy-mode-vi ` | | any custom table | Only after `switch-client -T `, and only for the very next keystroke | Whatever you bind into it | The `copy-mode` and `copy-mode-vi` tables are empty by default because the built in copy mode keymap is hardcoded. Your bindings in those tables are checked **first**, so binding a key there overrides the built in behavior for that key; every key you do not bind falls through to the built in map. `switch-client -T
` installs a one-shot table. The next key is looked up there, and the table is discarded whether or not the key matched. This is how multi key chords are built. Run `list-keys` (or `Prefix + ?`) to print every binding currently in effect. ### The root table is empty on purpose Nothing is bound without a prefix by default. In particular a bare `PageUp` is **not** a copy mode key: it is forwarded untouched so pagers, editors and full screen apps see it. This matches tmux, where `PPage` is bound only in the prefix table. To get the old psmux behavior back, bind it yourself: ```tmux bind-key -n PageUp copy-mode -u ``` One caveat with that recipe. The `scroll-enter-copy-mode` option suppresses any **root** binding whose command starts with `copy-mode` and contains `-u`. With `set -g scroll-enter-copy-mode off` the binding above stops firing and the key is forwarded to the pane instead. The prefix binding `Prefix + PageUp` is unaffected by that option. ### Overlay keys never reach a key table Overlay UI keys are handled client side and **earlier than any key table**. While a chooser, menu, confirmation prompt, customize view, keys viewer or the command prompt is open, the overlay consumes the keystroke. A key you bound with `bind-key` or `bind-key -n` will not fire, and the key is not forwarded to the pane either. Close the overlay first. The per overlay keymaps are listed below. ## Prefix Keys ### Sending the Prefix | Key | Action | |-----|--------| | `Prefix + Ctrl+b` | Send one literal prefix byte to the pane (`send-prefix`) | Because the prefix key is `Ctrl+b` by default, this is simply "press the prefix twice". If you rebind the prefix, the new key is bound to `send-prefix` for you. ### Window Management | Key | Action | |-----|--------| | `Prefix + c` | Create new window | | `Prefix + n` | Next window | | `Prefix + p` | Previous window | | `Prefix + l` | Last (previously active) window | | `Prefix + w` | Interactive session/window/pane chooser (`choose-tree`) | | `Prefix + &` | Kill current window (with confirmation) | | `Prefix + ,` | Rename current window | | `Prefix + '` | Prompt for a window index or name and select it (`select-window -t ':%%'`, as tmux) | | `Prefix + .` | Prompt for an index and move the current window there (`move-window`) | | `Prefix + 0-9` | Select window by number | `Prefix + '` is bound to `command-prompt -T window-target -p index "select-window -t ':%%'"`, as in tmux, so the prompt has the same editing keys and history as any other prompt. **Gotcha: `select-window-index` is not a real command.** Before #821 `Prefix + '` was bound to `select-window-index`, a client only name for a window index box of the client's own. A key bound to that name still opens the prompt above, so a config that binds it keeps working, but no command dispatcher accepts it: `psmux select-window-index` fails with "unknown command", and it cannot be used from a config line, from a hook, or from `run-shell`. ### Pane Splitting | Key | Action | |-----|--------| | `Prefix + %` | Split pane left/right (horizontal) | | `Prefix + "` | Split pane top/bottom (vertical) | ### Pane Navigation | Key | Action | |-----|--------| | `Prefix + Arrow` | Navigate between panes (Up/Down/Left/Right), wraps at edges | | `Prefix + o` | Select next pane (rotate) | | `Prefix + ;` | Last (previously active) pane | | `Prefix + q` | Display pane numbers (type number to switch, auto-dismisses) | ### Pane Management | Key | Action | |-----|--------| | `Prefix + x` | Kill current pane (with confirmation) | | `Prefix + z` | Toggle pane zoom (fullscreen) | | `Prefix + {` | Swap pane up | | `Prefix + }` | Swap pane down | | `Prefix + !` | Break pane out to new window | ### Pane Resize | Key | Action | |-----|--------| | `Prefix + Ctrl+Arrow` | Resize pane by 1 cell | | `Prefix + Alt+Arrow` | Resize pane by 5 cells | ### Layout | Key | Action | |-----|--------| | `Prefix + Space` | Cycle to next layout | | `Prefix + Alt+1` | Even-horizontal layout | | `Prefix + Alt+2` | Even-vertical layout | | `Prefix + Alt+3` | Main-horizontal layout | | `Prefix + Alt+4` | Main-vertical layout | | `Prefix + Alt+5` | Tiled layout | ### Session | Key | Action | |-----|--------| | `Prefix + d` | Detach from session | | `Prefix + $` | Rename session | | `Prefix + s` | Session chooser/switcher (`choose-session`) | | `Prefix + (` | Switch to previous session | | `Prefix + )` | Switch to next session | ### Copy / Paste | Key | Action | |-----|--------| | `Prefix + [` | Enter copy/scroll mode | | `Prefix + PageUp` | Enter copy mode scrolled up one page (`copy-mode -u`) | | `Prefix + ]` | Paste from buffer | | `Prefix + =` | Interactive buffer chooser (`choose-buffer`) | | `Prefix + #` | List paste buffers (`list-buffers`) | | `Prefix + v` | Toggle rectangle selection in copy mode (`rectangle-toggle`) | | `Prefix + y` | Copy the current selection (`copy-yank`) | `Prefix + v` and `Prefix + y` mirror the copy mode keys of the same name, so the muscle memory works whether or not you have entered copy mode through the prefix table. ### Miscellaneous | Key | Action | |-----|--------| | `Prefix + :` | Command prompt (with cursor, arrow key navigation, and history) | | `Prefix + ?` | List keybindings (help overlay) | | `Prefix + i` | Display window/pane info | | `Prefix + t` | Clock mode | ### Repeat Bindings Navigation and resize bindings support **repeat mode**: after pressing the prefix key once, successive keypresses within the `repeat-time` window (default 500ms) trigger the action without needing to re-enter the prefix. This applies to arrow-based pane navigation and resize bindings by default. ## Overlay Keys Each overlay owns its own keymap. Remember that these keys are consumed before any key table. ### Chooser Navigation (choose-tree, choose-session) Open with `Prefix + w` (tree) or `Prefix + s` (sessions). This matches tmux's `mode-tree` behavior, so muscle memory carries over. | Key | Action | |-----|--------| | `Up` / `k` / `C-p` | Move selection up | | `Down` / `j` / `C-n` | Move selection down | | `Left` / `h` / `-` | choose-tree: collapse the selected row (or its parent). choose-session: move up | | `Right` / `l` / `+` | choose-tree: expand the selected row. choose-session: move down | | `g` / `Home` | Jump to first entry | | `G` / `End` | Jump to last entry | | `PageUp` / `PageDown` / `C-b` / `C-f` | Move selection by one page (the height of the list) | | `0`..`9`, `M-a`.. | choose-tree: jump to that line and switch at once | | `1`..`9`, `0` | choose-session: append digit to the jump buffer (Enter consumes it) | | `Backspace` | Edit the jump buffer | | `Enter` | Switch to the selected entry (or to the jump buffer index if non-empty). A window or pane of another session switches to that session on that window or pane | | `f` | Enter session-name filter mode (choose-session only) | | `p` | Toggle live preview | | `x` | Kill the highlighted entry (session in choose-session, window in choose-tree), with confirmation | | `Esc` | Answer a pending kill question with no; otherwise clear an active session filter; otherwise close the chooser | | `q` / `C-g` | Close the chooser (`q` types into an active session filter instead) | In the session picker, `f` starts filter mode, which matches session names case-insensitively as you type. `Backspace` edits the filter. `Esc` with filter text entered clears the filter and shows every session again; `Esc` with no filter text closes the picker. `x` puts up a `y/n` question before it kills, as `Prefix + x` and `Prefix + &` do, and as tmux's chooser does. Only `y` kills; every other key cancels, including `Enter`, which inside a chooser is the key that activates a row. The chooser stays open behind the question either way. `q` closes these choosers as it closes every tmux mode. Every other printable key not listed above is swallowed so it cannot leak into the focused pane. The pane's cursor is hidden while a chooser is open, as tmux hides it for every mode tree mode. ### Buffer Chooser (choose-buffer) Open with `Prefix + =`. | Key | Action | |-----|--------| | `Up` / `k` / `h` | Move selection up | | `Down` / `j` / `l` | Move selection down | | `g` / `Home` | Jump to first buffer | | `G` / `End` | Jump to last buffer | | `C-p` / `C-n` | Move selection up / down | | `PageUp` / `PageDown` / `C-b` / `C-f` | Move selection by one page (the height of the list) | | `1`..`9`, `0` | Append digit to the jump buffer | | `Backspace` | Edit the jump buffer | | `Enter` | Paste the selected buffer (or the jump buffer index if non-empty) | | `d` / `Delete` | Delete the selected buffer and stay open | | `Esc` / `q` / `C-g` | Close the chooser | ### Keys Viewer (list-keys) Open with `Prefix + ?`. This overlay scrolls a rendered list, it has no selection. | Key | Action | |-----|--------| | `Up` / `k` / `h` | Scroll up one line | | `Down` / `j` / `l` | Scroll down one line | | `PageUp` / `PageDown` | Scroll by 20 lines | | `g` / `Home` | Jump to the top | | `G` / `End` | Jump to the bottom | | `Esc` / `q` | Close the viewer | ### Customize Mode (customize-mode) An interactive options editor. Navigation keys apply while browsing; a separate set applies once you press `Enter` on an option and start editing its value. Browsing: | Key | Action | |-----|--------| | `Up` / `k` / `h` | Move selection up | | `Down` / `j` / `l` | Move selection down | | `PageUp` / `PageDown` | Move selection by 20 rows | | `g` / `Home` | Jump to the first option | | `G` / `End` | Jump to the last option | | `1`..`9`, `0` | Append digit to the jump buffer | | `Backspace` | Edit the jump buffer | | `Enter` | Edit the highlighted option, or jump to the jump buffer index if non-empty | | `d` | Reset the highlighted option to its built in default | | `/` | Clear the active option filter | | `Esc` / `q` | Close the editor | Editing a value: | Key | Action | |-----|--------| | Any character | Insert at the cursor | | `Backspace` | Delete the character before the cursor | | `Enter` | Confirm the new value | | `Esc` | Cancel and keep the old value | Note on `/`: it only clears a filter that is already set. There is no way to type a new filter from inside the overlay, so `/` on an unfiltered list does nothing. ### Menu Overlay (display-menu) | Key | Action | |-----|--------| | `Up` / `k` | Move selection up | | `Down` / `j` | Move selection down | | `Enter` | Run the highlighted item | | Any other character | Run the item whose mnemonic key is that character | | `Esc` / `q` | Close the menu | Digits are mnemonics, not positions. A digit runs the item whose shortcut key is that digit, and does nothing if no item claims it. `q` closes the menu even if an item uses `q` as its mnemonic. ### Confirmation Prompt (confirm-before) | Key | Action | |-----|--------| | `y` / `Y` | Confirm and run the command | | `n` / `N` / `Esc` | Cancel | Every other key is ignored, so the prompt cannot be dismissed by accident. ### Text Popup Viewer Shown by commands that display text in a popup, for example `show-messages`. A `display-popup` running a real program is a PTY popup instead, and forwards keys to that program rather than using this map. | Key | Action | |-----|--------| | `Up` / `k` | Scroll up one line | | `Down` / `j` | Scroll down one line | | `PageUp` / `PageDown` | Scroll by 10 lines | | `g` / `Home` | Jump to the top | | `G` / `End` | Jump to the bottom | | `Esc` / `q` | Close the popup | ### Display Panes Overlay Shown by `Prefix + q`. | Key | Action | |-----|--------| | `0`..`9` | Select the pane with that number | | Any other key | Dismiss the overlay | ## Command Prompt Press `Prefix + :` to open the command prompt at the bottom of the screen. You can type any psmux/tmux command here. | Key | Action | |-----|--------| | `Left` / `Right` | Move cursor within the command | | `Home` / `Ctrl+A` | Jump to start of line | | `End` / `Ctrl+E` | Jump to end of line | | `Backspace` | Delete character before cursor | | `Delete` | Delete character at cursor | | `Ctrl+U` | Kill line (clear to start) | | `Ctrl+K` | Kill to end of line | | `Ctrl+W` | Delete word backward | | `Up` / `Down` | Browse command history (older/newer) | | `Tab` | Command name completion | | `Enter` | Execute the command (saved to history) | | `Escape` | Cancel and close the prompt | The command prompt remembers your history across the session. Use Up/Down arrows to recall previous commands. Inspect it with `show-prompt-history` and wipe it with `clear-prompt-history`. You can run any command from the prompt that you would run from the CLI. For example: - `:split-window -h` to split horizontally - `:new-window -n logs` to create a named window - `:source-file ~/.psmux.conf` to reload your config - `:set -g status-style "bg=blue"` to change a setting live - `:list-keys` to see all current key bindings ## Copy/Scroll Mode (Vi) Enter copy mode with `Prefix + [`, or with `Prefix + PageUp` to start one page up. Mouse scroll wheel also enters copy mode by default. To disable this, set `scroll-enter-copy-mode off` in your config. Copy mode belongs to the pane it was entered in, as in tmux. Splitting, opening a window or killing another pane leaves that pane in copy mode and starts the new or newly focused pane live; several panes can be in copy mode at once, and `#{pane_in_mode}` answers for the pane you target. The keys below are the built in copy mode map. Anything you bind with `bind-key -T copy-mode-vi ` is checked first and wins for that key. ### Cursor Movement | Key | Action | |-----|--------| | `h` / `Left` | Move cursor left | | `j` / `Down` | Move cursor down | | `k` / `Up` | Move cursor up | | `l` / `Right` | Move cursor right | | `Ctrl+p` / `Ctrl+n` | Move cursor up / down (same as `k` / `j`, emacs style, works in vi mode too) | | `Ctrl+a` | Start of line (emacs style, works in vi mode too) | | `Ctrl+e` | End of line, **`mode-keys emacs` only** (in vi mode `Ctrl+e` scrolls, use `$`) | Cursor motions keep the viewport still until the cursor reaches the top or the bottom row of the pane. Only then does the view follow, one line at a time. To move the view itself while the cursor stays put, use the scrolling keys below. ### Word Motions | Key | Action | |-----|--------| | `w` / `b` / `e` | Next word / prev word / end of word | | `W` / `B` / `E` | WORD variants (whitespace-delimited) | | `Alt+f` / `Alt+b` | Word forward / backward (emacs style, works in vi mode too) | ### Line Motions | Key | Action | |-----|--------| | `0` / `Home` | Start of line | | `$` / `End` | End of line | | `^` | First non-blank character | ### Scrolling | Key | Action | |-----|--------| | `Ctrl+u` / `Ctrl+d` | Half page up / down | | `Ctrl+b` / `PageUp` | Full page up | | `Ctrl+f` / `PageDown` | Full page down | | `Ctrl+Up` / `Ctrl+Down` | Scroll up / down one line (cursor stays where it is) | | `Ctrl+y` / `Ctrl+e` | Scroll up / down one line, **`mode-keys vi` only** | | `K` / `J` | Scroll up / down one line, **`mode-keys vi` only** | | `g` | Top of scrollback | | `G` | Bottom (live output) | | `z` | Centre the cursor line in the pane (scroll-middle) | | `r` | Copy the pane's current output into copy mode once (refresh-from-pane, see below) | A full page is the pane height minus two lines and a half page is half the pane height, the same amounts tmux uses (`window_copy_pageup1` in `window-copy.c`). A pane two rows tall or shorter moves one line. The view moves and the cursor stays on its row, except at the top or bottom of the history, where the cursor is pulled along so repeated presses reach the first or last line. Copy mode reads a copy of the pane taken when it opened, so new output cannot shift the text under your cursor. `r` is tmux's `refresh-from-pane`: it takes that copy again once, so everything the pane printed since becomes reachable, and it keeps the line at the top of the view where it was, the way tmux 3.7 does (`window_copy_cmd_refresh_from_pane` in `window-copy.c`). The new lines land below the view; `G` goes to them. Like tmux it clears the selection and stays in copy mode. `send-keys -X refresh-now` is the same command under the name OpenBSD's current manual uses. To follow output continuously instead, tmux after 3.7c has an automatic refresh, and psmux has it too: `send-keys -X refresh-on`, `refresh-off` and `refresh-toggle`. While it is on, copy mode takes a new copy whenever the pane prints. It follows the output while the cursor sits on the last row at the live bottom, keeps your place anywhere else, and pauses while a selection is being made. To get the newer tmux binding: ```tmux bind-key -T copy-mode-vi r send-keys -X refresh-toggle ``` ### Screen Position | Key | Action | |-----|--------| | `H` | Jump to top of visible area | | `M` | Jump to middle of visible area | | `L` | Jump to bottom of visible area | | `P` | Hide or show the position indicator in the top-right corner | ### Character Find | Key | Action | |-----|--------| | `f{char}` / `F{char}` | Find char forward / backward | | `t{char}` / `T{char}` | Till char forward / backward | | `;` | Repeat the last find in the same direction | | `,` | Repeat the last find in the opposite direction | ### Marks | Key | Action | |-----|--------| | `X` | Set the mark at the cursor | | `Alt+x` | Exchange the cursor and the mark | `Alt+x` swaps rather than jumps, matching tmux. Pressing it twice returns you to where you started. The marked line is drawn in `copy-mode-mark-style` (`bg=red,fg=black`) with the marked cell's colours swapped, as tmux draws it. ### Bracket / Paragraph | Key | Action | |-----|--------| | `%` | Jump to matching bracket (`()`, `[]`, `{}`, `<>`) | | `{` | Jump to previous paragraph (blank line) | | `}` | Jump to next paragraph (blank line) | ### Selection | Key | Action | |-----|--------| | `Space` | Begin character selection at the cursor | | `V` | Begin line selection at the cursor | | `Ctrl+Space` | Set the selection anchor at the cursor | | `v` / `Ctrl+v` | Toggle rectangle mode (does not start a selection) | | `o` | Swap cursor/anchor ends | | `Esc` | Clear the selection and stay in copy mode (`mode-keys vi`) | **psmux follows tmux here, not vi.** In vi, `v` starts a character selection. In tmux and in psmux, `v` is `rectangle-toggle`: it flips the selection between character mode and block mode and does not set an anchor, so pressing `v` on its own selects nothing. Use `Space` (or `Ctrl+Space`) to start a selection and `V` to start a line selection. Pressed before `Space`, `v` makes the selection a rectangle from the start, as in tmux. Builds before discussion #694 began a character selection on `v` despite this table; if you got used to that, rebind it: ```tmux bind-key -T copy-mode-vi v send-keys -X begin-selection ``` ### Yank (Copy) | Key | Action | |-----|--------| | `y` / `Enter` | Copy selection and exit | | `Alt+w` | Copy selection and exit (emacs style, works in vi mode too) | | `D` | Copy from the cursor to end of line and exit | | `A` | Append selection to the buffer and exit | ### Search | Key | Action | |-----|--------| | `/` | Search forward | | `?` | Search backward | | `Ctrl+s` / `Ctrl+r` | Search forward / backward (emacs style, works in vi mode too) | | `n` / `N` | Next / previous match | Search covers the whole scrollback, not just the rows on screen. A match that is off screen scrolls the view to it, parked a quarter of a screen from the bottom the way tmux does, and `?` stops at the nearest match above the cursor. Every match on screen is drawn in `copy-mode-match-style` (`bg=cyan,fg=black`) and the one under the cursor in `copy-mode-current-match-style` (`bg=magenta,fg=black`), tmux's defaults. As in tmux the highlight stays while you move with `n`, `N` and the cursor keys, and goes when a command that changes the selection or the mark runs (`Space`, `Esc`, `V`, `v`, `X`, `z`, `%`, `^`). ### Goto Line | Key | Action | |-----|--------| | `:` | Ask for a line number and move the view to it (`mode-keys vi` only) | See [Copy Mode Goto Line Prompt](#copy-mode-goto-line-prompt) for what the number counts. ### Text Objects & Registers | Key | Action | |-----|--------| | `"a` to `"z` | Named registers (set register for next yank) | | `aw` / `iw` | Select a word / inner word | | `aW` / `iW` | Select a WORD / inner WORD | | `1` to `9` | Numeric prefix for motions (up to 9999) | ### Exit | Key | Action | |-----|--------| | `q` / `]` | Exit copy mode | | `Esc` | Clear the selection and stay in copy mode with `mode-keys vi`; exit with `mode-keys emacs` | | `Ctrl+C` / `Ctrl+G` | Exit copy mode | ### Copy Mode Search Prompt Opened by `/`, `?`, `Ctrl+s` or `Ctrl+r`. While it is open the copy mode keys are inactive. | Key | Action | |-----|--------| | Any character, `Space` included | Insert at the cursor | | `Left` / `Right` | Move the cursor | | `Home` / `Ctrl+a` | Move to the start | | `End` / `Ctrl+e` | Move to the end | | `Backspace` | Delete the character before the cursor | | `Delete` / `Ctrl+d` | Delete the character under the cursor | | `Ctrl+u` / `Ctrl+k` / `Ctrl+w` | Clear the line / cut to the end / cut the word before the cursor | | `Up` / `Down` | Recall earlier searches | | `Enter` | Accept the search and jump to the match | | `Esc` / `Ctrl+c` / `Ctrl+g` | Cancel the search | These are the keys of tmux's prompt editor that apply here. The cursor is drawn on the status line where it sits, and the prompt scrolls when the search term is longer than the line, so the cursor stays on screen. `Ctrl+b` and `Ctrl+f` also move the cursor, but `Ctrl+b` is the default prefix, which the client takes first. The goto line prompt takes the same editing keys. ### Copy Mode Goto Line Prompt Opened by `:` with `mode-keys vi`, which is the key tmux binds it to in `copy-mode-vi`. While it is open the copy mode keys are inactive. | Key | Action | |-----|--------| | Any character | Append to the line number | | `Backspace` | Delete the last character | | `Enter` | Move the view to that line | | `Esc` | Cancel and leave the view where it was | What the number counts follows `copy-mode-line-numbers`, so it always matches the gutter on screen: | `copy-mode-line-numbers` | The number means | |-----|--------| | `absolute`, `relative`, `hybrid` | A line of the whole buffer, counting the oldest retained line as 1 | | `off`, `default` | How many lines back from the live bottom to scroll | A line number past the end is clamped to the end, and anything that is not a number leaves the view alone. The same rules apply to `send-keys -X goto-line `. ### Emacs Copy Mode `set -g mode-keys emacs` does not add a separate keymap. Most emacs style keys listed above are always active, in vi mode too. What `mode-keys emacs` changes is the meaning of the keys tmux binds differently in its `copy-mode` and `copy-mode-vi` tables: | Key | With `mode-keys vi` | With `mode-keys emacs` (default) | |-----|--------|--------| | `Ctrl+b` | Page up | Move cursor left | | `Ctrl+f` | Page down | Move cursor right | | `Ctrl+v` | Force rectangle selection | Page down | | `Ctrl+e` | Scroll down one line | End of line | | `Ctrl+y` | Scroll up one line | Nothing | | `J` / `K` | Scroll down / up one line | Nothing | | `g` / `G` | History top / bottom | Nothing (use `M-<` / `M->`) | In vi mode `Ctrl+e` scrolls, so use `$` (or `End`) to reach the end of the line there. This is tmux's split: `copy-mode` binds `Ctrl+e` to end-of-line and leaves `Ctrl+y`, `J` and `K` unbound, while `copy-mode-vi` binds `Ctrl+e` and `J` to scroll down and `Ctrl+y` and `K` to scroll up. tmux spells history-top/history-bottom `g` / `G` in `copy-mode-vi` only; the `copy-mode` table uses `M-<` / `M->`. psmux used to answer a bare `g` in both tables, so a stray `g` - a prompt letter typed into a pane that was still in copy mode, say - threw the view to the top of the scrollback. `g` / `G` are now vi-only and `M-<` / `M->` work in both. These keys behave the same under both settings: `Ctrl+p` / `Ctrl+n` move the cursor up / down, `Ctrl+Up` / `Ctrl+Down` scroll one line, `Ctrl+a` goes to line start, `Alt+f` / `Alt+b` move by word, `Alt+v` pages up, `Alt+w` copies and exits, `Ctrl+s` / `Ctrl+r` search forward / backward. `Ctrl+p` / `Ctrl+n` are cursor motions, not scrolling, which is what tmux binds them to in its `copy-mode` table. psmux keeps them available in vi mode as well, where tmux leaves them unbound. When in copy mode: - The pane border turns **yellow** - `[copy mode]` appears in the title - A position indicator shows in the top-right corner, which `P` hides and shows - Mouse drag-select copies to Windows clipboard on release `P` is tmux's `toggle-position`, bound in both copy-mode tables. The indicator comes back the next time you enter copy mode, because the setting belongs to that visit rather than to the pane. `copy-mode -H` opens copy mode with it already hidden. ## Mouse Bindings When `mouse on` (default): | Action | Behavior | |--------|----------| | Left-click status tab | Switch to clicked window | | Left-click pane | Focus that pane | | Left-click/drag border | Resize split interactively | | Scroll up/down | Scroll pane (or enter copy mode at prompt) | | Mouse drag in copy mode | Select text, auto-copy on release | | Drag held on the pane's first or last row | Auto-scroll the selection through scrollback (faster once the pointer leaves the pane) | | Right-click | Paste clipboard | ## Supported Key Names Key names for `bind-key` and `send-keys`: | Key | Name | |-----|------| | Arrow keys | `Up`, `Down`, `Left`, `Right` | | Function keys | `F1` through `F12` | | Special keys | `Enter`, `Tab`, `Escape`, `Space`, `Backspace` | | Navigation | `Home`, `End`, `PageUp`, `PageDown`, `Insert`, `Delete` | | Ctrl modifier | `C-a` through `C-z`, `C-Space` | | Alt modifier | `M-a` through `M-z`, `M-Left`, `M-Right`, etc. | | Shift+key | Use uppercase letter: `T` for `Shift+t` | | Shift+Enter | `S-Enter` (sends proper escape sequence) | | Shift+Tab | `BTab` (sends `ESC [ Z`) | | Ctrl+Backspace | `C-BSpace` (writes `0x08`, the native Windows encoding, so PSReadLine runs BackwardKillWord; `M-BSpace` writes `ESC 0x7f`). A terminal that sends a bare `0x08` for Ctrl+Backspace is seen as `C-h`, which is also what tmux reports for it | | Non-ASCII keys | Any single Unicode character: `ы`, `é`, `ß`, with the usual modifiers (`M-ф`, `C-ы`, `S-ы`). `list-keys` prints the character as typed | | Mouse key names | tmux's mouse names (`WheelUpPane`, `MouseDown1Pane`, `MouseDrag1Border`, `DoubleClick1Pane`, `WheelDownStatus`, and so on, with `C-`, `M-` and `S-`) are real keys: `bind`, `unbind` and `list-keys` take them. The wheel over a pane runs them (see [Mouse key bindings](#mouse-key-bindings)); the other mouse names are stored and listed but psmux's built-in mouse handling still acts on clicks, drags and the status line. `Any`, `None`, `Mouse`, `FocusIn`/`FocusOut`, `PasteStart`/`PasteEnd` and `UserN` are accepted so a ported tmux config loads, but psmux does not act on them | Aliases are accepted for several of these: `Return` for `Enter`, `Esc` for `Escape`, `BSpace` for `Backspace`, `PPage` or `PgUp` for `PageUp`, `NPage` or `PgDn` for `PageDown`, `IC` for `Insert`, `DC` for `Delete`, `BackTab` for `BTab`. ### Mouse key bindings A wheel notch over a pane is the tmux key `WheelUpPane` or `WheelDownPane`, with `C-`, `M-` or `S-` when a modifier is held, and it is looked up the way tmux looks it up: in the `copy-mode` (or `copy-mode-vi`) table while the pane is in copy mode, then in `root`. | Situation | What happens | |-----------|--------------| | Nothing changed | The built-in defaults, listed by `list-keys`: root `WheelUpPane` enters copy mode and scrolls, the copy-mode `WheelUpPane` / `WheelDownPane` scroll 3 lines and leave copy mode at the bottom, and a program that asked for the mouse gets the wheel | | `bind -n WheelUpPane ...`, `bind -T copy-mode-vi WheelDownPane ...` | Your command runs instead. `-t =`, `-t=` and `{mouse}` name the pane under the pointer, and `send-keys -M` hands the wheel to the program in it | | `unbind -n WheelUpPane` | The key is gone, as in tmux: the wheel no longer enters copy mode, and only reaches a program that enabled mouse reporting | | A modified wheel (`C-WheelUpPane`) with nothing bound | As in tmux: a program with mouse reporting on gets it (with the Ctrl bit), a shell prompt gets nothing, copy mode ignores it | | `set -g ctrl-wheel-zoom on` | Ctrl+wheel in Windows Terminal zooms the font before any binding is consulted | | The wheel over the status line | `WheelUpStatus` / `WheelDownStatus`, `previous-window` / `next-window` by default as in tmux; bind or unbind them like any key | | The end of a drag (`MouseDragEnd1Pane` in `copy-mode` / `copy-mode-vi`) | The default copies and leaves copy mode. `unbind -T copy-mode-vi MouseDragEnd1Pane` keeps the selection in copy mode after the drag, and a binding (`send -X copy-selection-no-clear`, ...) runs instead. A drag at the shell prompt follows it too: with the default changed, the drag ends in copy mode, as in tmux | The press and drag of the left button in a pane (`MouseDown1Pane`, `MouseDrag1Pane`), a drag on a pane separator (`MouseDrag1Border`), a click on the status line (`MouseDown1Status`) and the right button (`MouseDown3Pane`) follow the same rules: while a key keeps its built-in default psmux behaves exactly as before; a binding (with or without `C-` / `M-`) runs instead; unbound, the event goes to the program only if it asked for the mouse. A binding to tmux's own default command for a key (`bind -n MouseDrag1Border resize-pane -M`, `bind -n MouseDown1Status switch-client -t=`) keeps psmux's built-in behaviour. Double and triple clicks follow tmux's click timer: a second left press within 300 ms of the first is `SecondClick1Pane` (no default), a third within 300 ms of the second is `TripleClick1Pane`, and when the 300 ms run out after a second press it is `DoubleClick1Pane`. The defaults are tmux's: a double click selects the word under the pointer and a triple click the line, enter copy mode for it, show it for 0.3 s, copy it (paste buffer and Windows clipboard) and leave copy mode; over a program that asked for the mouse the program gets the clicks instead, and with `pwsh-mouse-selection` on psmux's own word and line selection is used. The status line has `SecondClick1Status`, `DoubleClick1Status` and `TripleClick1Status` with no default. The window is 300 ms as in tmux, which has no option for it (`KEYC_CLICK_TIMEOUT`). ### Right click paste in programs that track the mouse A program that turns on mouse reporting (Claude Code, OpenCode, vim with `mouse=a`) receives the right click itself, as in tmux, so psmux's right click paste does not happen there. To paste the Windows clipboard on a right click everywhere, through the bracketed paste route the program expects ([#796](https://github.com/psmux/psmux/issues/796)): ```tmux bind -n MouseDown3Pane { refresh-client -l ; paste-buffer -p } ``` or keep the plain right click and paste on Alt+right click: ```tmux bind -n M-MouseDown3Pane { refresh-client -l ; paste-buffer -p } ``` `refresh-client -l` stores the Windows clipboard as a new paste buffer (tmux asks the terminal for its clipboard; psmux reads it directly, so it is there before `paste-buffer` runs), and `paste-buffer -p` wraps it in bracketed paste markers when the program asked for them. A binding's command list can be written as a tmux brace block, on one line or several: ``` bind -n F8 { set -g @a 1; set -g @b 2 } bind -n F9 { if -F '#{pane_in_mode}' { send -X cancel } { copy-mode } } ``` `list-keys` prints the block as its command list (`set -g @a 1 \; set -g @b 2`). ### Extended keys from the host terminal A host terminal that reports modified keys in the `CSI u` form (Windows Terminal with extended keys on, WezTerm, Kitty, xterm with `modifyOtherKeys`) is understood on both input paths, so `Shift+Enter`, `Ctrl+Enter` and the like reach a bound key or the pane as the key they are (#654). A `CSI u` that arrives inside a paste stays text. Known boundary: under Git Bash's mintty the psmux client runs behind Cygwin's pseudo console, and that conhost drops a `CSI u` it does not recognise before psmux sees it when virtual terminal input is off. Modified keys reported that way do not reach psmux there. This is a limitation of the host, not something psmux can recover; Windows Terminal and WezTerm are the hosts where extended keys are verified. A key name psmux cannot parse is never dropped silently. `psmux bind-key WheelUpPain ...` prints `unknown key: WheelUpPain` and exits 1, like tmux; the same line in a config file is reported in the boot summary and in `~/.psmux/config-warnings.log` (see [diagnostics.md](diagnostics.md)). ## Custom Key Bindings ```tmux # Bind in prefix table (default) bind-key h split-window -h bind-key v split-window -v # Bind in root table (no prefix needed) bind-key -n C-h select-pane -L # Restore the pre-issue-488 bare PageUp behavior bind-key -n PageUp copy-mode -u # Bind inside copy mode bind-key -T copy-mode-vi v send-keys -X begin-selection # Repeatable binding (stay in prefix mode) bind-key -r H resize-pane -L 5 # Unbind a key unbind-key C-b # Unbind all unbind-key -a ``` Default bindings are loaded into the `prefix` table like any other binding, so `unbind-key ` really does remove a default rather than being shadowed by it. ## Confirmation Prompts (confirm-before) By default, destructive keybindings like `Prefix + x` (kill-pane) and `Prefix + &` (kill-window) show a y/n confirmation prompt before executing. This uses the `confirm-before` wrapper, matching tmux behavior. ### Skipping Confirmation To bind kill commands **without** confirmation, bind the command directly in your config: ```tmux # Kill pane immediately (no y/n prompt) bind-key x kill-pane # Kill window immediately (no y/n prompt) bind-key & kill-window # Kill session on a custom key (no prompt) bind-key X kill-session ``` ### Adding Confirmation to Any Command You can wrap any command with `confirm-before` to require y/n confirmation: ```tmux # Confirm before killing pane (this is the default) bind-key x confirm-before -p 'kill-pane #P? (y/n)' kill-pane # Confirm before killing window (this is the default) bind-key & confirm-before -p 'kill-window #W? (y/n)' kill-window # Confirm before killing session bind-key X confirm-before -p 'kill-session? (y/n)' kill-session # Confirm before detaching bind-key d confirm-before -p 'detach? (y/n)' detach-client ``` The `-p` flag sets a custom prompt string. Without it, a generic prompt is shown. --- # psmux Plugins and Themes: the tmux plugin ecosystem on Windows psmux plugins are ports of the most popular tmux plugins, reimplemented in PowerShell so they run natively on Windows without bash, cygwin, or WSL. A psmux plugin works exactly the way a tmux plugin works: a script that calls the multiplexer's own CLI (`set-option`, `bind-key`, `set-hook`, `run-shell`) to configure it. The commands are identical to tmux; only the scripting language around them changed from bash to PowerShell. tmux themes that are plain option lines work in psmux verbatim. **Key facts** - Plugin manager: **ppm** (the tmux `tpm` equivalent), declared with `set -g @plugin '...'` and loaded with a single `run` line, installed with `Prefix + I`. - Plugins live in `~/.psmux/plugins//` (or `$XDG_CONFIG_HOME/psmux/plugins/` as a fallback). - Entry point: `/.ps1` (PowerShell) or `/plugin.conf` (pure tmux option lines). - psmux reads `@plugin` declarations itself during config parsing and sources the plugin's `plugin.conf` synchronously, so themes apply before the first frame (no flash). - tmux `.tmux` entry scripts are translated statically when referenced from a config `run` line; full bash plugins can also be executed through Git Bash. - Browse plugins: [psmux-plugins](https://github.com/psmux/psmux-plugins). Install with a TUI: [Tmux Plugin Panel](https://github.com/psmux/Tmux-Plugin-Panel). Related: [Configuration](configuration.md) · [Key Bindings](keybindings.md) · [Scripting](scripting.md) · [Dev environment layouts](tutorials/dev-environment-layouts.md) ## Plugin Repository **Browse available plugins and themes:** [**psmux-plugins**](https://github.com/psmux/psmux-plugins) **Install & manage plugins with a TUI:** [**Tmux Plugin Panel**](https://github.com/psmux/Tmux-Plugin-Panel), a terminal UI for browsing, installing, updating, and removing plugins and themes. ## Available Plugins | Plugin | Description | |--------|-------------| | [ppm](https://github.com/psmux/psmux-plugins/tree/main/ppm) | Plugin manager (like tpm) | | [psmux-sensible](https://github.com/psmux/psmux-plugins/tree/main/psmux-sensible) | Sensible defaults for psmux | | [psmux-yank](https://github.com/psmux/psmux-plugins/tree/main/_trash/psmux-yank) | Windows clipboard integration (retired: psmux copies to the Windows clipboard natively, see below) | | [psmux-resurrect](https://github.com/psmux/psmux-plugins/tree/main/psmux-resurrect) | Save/restore sessions | | [psmux-continuum](https://github.com/psmux/psmux-plugins/tree/main/psmux-continuum) | Auto save/restore sessions (works with resurrect) | | [psmux-pain-control](https://github.com/psmux/psmux-plugins/tree/main/psmux-pain-control) | Better pane navigation | | [psmux-vim-navigator](https://github.com/psmux/psmux-plugins/tree/main/psmux-vim-navigator) | Prefix-less `Ctrl-h/j/k/l` pane navigation that passes through to vim, nvim and fzf | | [psmux-prefix-highlight](https://github.com/psmux/psmux-plugins/tree/main/psmux-prefix-highlight) | Prefix key indicator | | [psmux-battery](https://github.com/psmux/psmux-plugins/tree/main/psmux-battery) | Battery status in status bar | | [psmux-cpu](https://github.com/psmux/psmux-plugins/tree/main/psmux-cpu) | CPU usage in status bar | | [psmux-net-speed](https://github.com/psmux/psmux-plugins/tree/main/psmux-net-speed) | Network speed in status bar | | [psmux-git-status](https://github.com/psmux/psmux-plugins/tree/main/psmux-git-status) | Git branch and status in status bar | | [psmux-sidebar](https://github.com/psmux/psmux-plugins/tree/main/psmux-sidebar) | File tree sidebar | | [psmux-logging](https://github.com/psmux/psmux-plugins/tree/main/psmux-logging) | Log pane output to files | ## Themes | Theme | Description | |-------|-------------| | [Catppuccin](https://github.com/psmux/psmux-plugins/tree/main/psmux-theme-catppuccin) | Soothing pastel theme (Latte, Frappe, Macchiato, Mocha) | | [Dracula](https://github.com/psmux/psmux-plugins/tree/main/psmux-theme-dracula) | Dark theme with vibrant colors | | [Nord](https://github.com/psmux/psmux-plugins/tree/main/psmux-theme-nord) | Arctic, north bluish color palette | | [Tokyo Night](https://github.com/psmux/psmux-plugins/tree/main/psmux-theme-tokyonight) | Clean dark theme inspired by Tokyo at night | | [Gruvbox](https://github.com/psmux/psmux-plugins/tree/main/psmux-theme-gruvbox) | Retro groove color scheme | | [Everforest](https://github.com/psmux/psmux-plugins/tree/main/psmux-theme-everforest) | Comfortable green based color scheme | | [Kanagawa](https://github.com/psmux/psmux-plugins/tree/main/psmux-theme-kanagawa) | Dark theme inspired by Katsushika Hokusai | | [One Dark](https://github.com/psmux/psmux-plugins/tree/main/psmux-theme-onedark) | Atom One Dark inspired theme | | [Rose Pine](https://github.com/psmux/psmux-plugins/tree/main/psmux-theme-rosepine) | Soho vibes for the terminal | | [Warm Burnout](https://github.com/psmux/psmux-plugins/tree/main/psmux-theme-warm-burnout) | Warm, low contrast theme (dark and light) | ## Quick Start ```powershell # Install the plugin manager git clone https://github.com/psmux/psmux-plugins.git "$env:TEMP\psmux-plugins" Copy-Item "$env:TEMP\psmux-plugins\ppm" "$env:USERPROFILE\.psmux\plugins\ppm" -Recurse Remove-Item "$env:TEMP\psmux-plugins" -Recurse -Force ``` Then add to your `~/.psmux.conf`: ```tmux set -g @plugin 'psmux-plugins/ppm' set -g @plugin 'psmux-plugins/psmux-sensible' run '~/.psmux/plugins/ppm/ppm.ps1' ``` Press `Prefix + I` inside psmux to install the declared plugins. `Prefix + U` updates them, `Prefix + M` removes plugins that are no longer declared. ## How do psmux plugins work? The mechanism is the tmux one. A tmux plugin is a bash script that runs `tmux set-option ...` and `tmux bind-key ...`. A psmux plugin is a PowerShell script that runs `psmux set-option ...` and `psmux bind-key ...`. Everything after the binary name is the same text. ``` tmux plugin (bash): tmux set-option -g mouse on psmux plugin (PowerShell): psmux set-option -g mouse on ``` The pieces of psmux that a plugin relies on, and where each lives in the source: | Mechanism | What psmux does | Source | |-----------|-----------------|--------| | `set -g @plugin 'owner/name'` | The config parser stores the value as a user option, then looks for `~/.psmux/plugins//plugin.conf` and sources it on the spot. If there is no `plugin.conf` it looks for `/.ps1`, extracts literal `set`/`bind` lines from it, and queues the script for execution at server start if it uses PowerShell variables. XDG paths under `$XDG_CONFIG_HOME/psmux/plugins/` are tried as well. | `src/config.rs` (`@plugin` branch in the `set` handler, `parse_ps1_plugin_script`) | | `run '~/.psmux/plugins/ppm/ppm.ps1'` in the config | A config `run`/`run-shell` line spawns the command without blocking. `~` expands to your profile, and a `~/.psmux/plugins/` path is redirected to the XDG directory when only that one exists. The child gets `PSMUX_TARGET_SESSION` so its `psmux` calls reach the server that is starting up. | `src/config.rs` (`parse_run_shell`), `src/util.rs` (`expand_run_shell_path`) | | Queued `.ps1` plugin scripts | After the config is loaded the server runs each queued script with `pwsh -NoProfile -ExecutionPolicy Bypass -File