feat(psmux): add native-Windows tmux multiplexer module#10
Merged
Conversation
…@0f46ccc) Third-party persistence plugins vendored + SHA-pinned rather than installed via PPM. PPM's resolver clones each plugin from github.com/psmux-plugins/<name> first — an unregistered, attacker-registrable org (HTTP 404) — then runs it at startup (namespace-hijack -> RCE). These are loaded via source-file of their static plugin.conf instead, so PPM is never used. See psmux/plugins/README.md.
Adopt psmux (winget: marlocarlo.psmux) for one-key pane equalize (select-layout tiled) that zellij has no native action for. zellij is kept installed as a fallback; retirement trigger documented in CLAUDE.md. Config (~/.psmux.conf): - auto-grid: new panes re-tile to an even grid via a toggled after-split-window hook; Prefix+g toggles live, status bar shows [grid] when armed - unified Ctrl-hjkl navigation shared with nvim (nvim needs no change: psmux sets $TMUX, existing keymaps.lua handles the edge handoff) - sticky resize mode, copy-on-select, native catppuccin-mocha theme + sensible defaults inlined (no theme/sensible plugins) - reboot-persistence via the vendored resurrect/continuum plugins, source-file'd directly (no PPM: closes the psmux-plugins namespace-hijack RCE) - setup.ps1: -Module psmux (symlink ~/.psmux.conf, copy vendored plugins) - powershell profile Phase 2b: Enter/Remove/Get-PsmuxSession fzf pickers + native session-name tab-completion for psmux/pmux/tmux - tests/psmux.Tests.ps1: loads the config end-to-end and asserts persistence binds - CI: exclude vendored psmux/plugins from the PSScriptAnalyzer gate
`pane-border-status top` renders a pane-title row that psmux miscounts, so the shell prompt/cursor lands a row off. Remove the border display; Prefix+T still sets the pane title, it's just no longer shown in the border.
Replaces the removed pane-border title display. The focused pane's title appears at the left of the status bar once set via Prefix+T, and is hidden while it's still the default hostname. Non-intrusive (status bar, not a pane row), so it doesn't reintroduce the cursor misalignment.
The native completer already filled in session names after attach/kill-session/ etc.; extend it so the subcommand position completes too (`psmux a<TAB>` -> attach/attach-session), sourced from `list-commands` (names + aliases, works with no server running).
- Session fzf helpers (Get-/Enter-/Remove-PsmuxSession) were bare `function` inside Initialize-DeferredProfileSecondary (Phase 2b), so they scoped local and vanished on return. Declare `function global:` (matches the ll/la/lt precedent); the tab-completer was unaffected (Register-ArgumentCompleter is a global side-effect). - Resize-mode (Prefix+r then hjkl) relied on a custom key-table (switch-client -T resize) that psmux does not support, so it degraded to a session-switch. Remove it (native Prefix+Ctrl-arrows resizes) and rebind Prefix+r to reload the config. - auto-grid Prefix+g toggle: switch the parse-time-frozen `if-shell -F` to a runtime sh `if-shell`. Still does not re-enable live (known issue, tracked for follow-up); auto-grid default-on is unaffected. - Docs (CLAUDE.md, psmux/README) updated; fix stale README bits (phantom move-mode, pane title now shown in the status bar not the pane border).
- Prefix+g auto-grid toggle: switch if-shell -> run-shell. psmux's
if-shell in a bind always runs the true branch, so it could never
re-enable. run-shell moves the decision out of psmux; it runs its
arg as PowerShell (not sh), so the body is pwsh if/else with
@auto_grid quoted to avoid splatting.
- New panes open in the focused pane's directory: the prompt now emits
OSC 7 (psmux reads pane_current_path from OSC 7, not OSC 9;9), and
split/new-window are rebound with -c "#{pane_current_path}".
…s fzf cursor desync PSFzf launches fzf via a redirected-stdout System.Diagnostics.Process from a PSReadLine handler, which desyncs fzf's screen/cursor under psmux's ConPTY (doubled UI, corrupted input). Reimplement the interactive pickers on bare fzf that SELF-POPULATES so fzf owns the tty (no stdin pipe): - Ctrl+t: fzf --multi self-populating from FZF_DEFAULT_COMMAND - Alt+c: temporary FZF_DEFAULT_COMMAND override to the fd dir-walker - Ctrl+r: stage PSReadLine history to a temp file, fzf self-populates via cmd type - Alt+b/Alt+g: Invoke-Fzf -> bare fzf Disable PSFzf's default chord binds (their param defaults clobber ours) and bind ours after Set-PsFzfOption. Exclude AppData/node_modules from the fd walkers for speed. TDD pure helpers (quote/join, history dedup); 10/10 Pester pass. Prompt: fzf's Windows renderer can leave newline-auto-return off and the cursor mid-row on abort; harden Set-Prompt.ps1 with a leading cursor-to-column-0 and CR+LF+clear-to-EOL between lines so the prompt renders at the left margin. KNOWN ISSUE: Ctrl+r still does not spawn fzf (temp-file self-populate not yet working).
…zellij defaults - windowsterminal: bind ctrl+backspace to sendInput of the Ctrl+W byte (U+0017) so backward-delete-word survives a multiplexer dropping the Control modifier (upstream of psmux/zellij; PSReadLine/readline/fzf all map Ctrl+W to it) - psmux: escape-time 0 (a lone Esc reaches TUIs; fixes fzf abort). @auto_grid OFF by default -- the after-split-window 'select-layout tiled' hook steals focus from the new pane and psmux hooks take only one command; Prefix+Alt+5 equalizes on demand, Prefix+g re-enables auto-tile - zellij: auto_layout false (panes stay put; swap-layout auto-grid abandoned, it would not auto-tile on 0.44.3) + locked-mode targeted splits (Alt+- / Alt-bslash)
psmux is the daily driver (Zellij's swap-layout auto-grid won't auto-tile); document the ConPTY fixes (Ctrl+Backspace, escape-time 0, bare-fzf pickers), the auto-grid focus tradeoff (@auto_grid off), and zellij auto_layout false.
…modes
The empty-buffer Ctrl+r path built fzf args as `$x = if (...) { @('--no-sort') }`.
PowerShell unwraps a single-element array returned from an `if` block to the scalar
string '--no-sort', so `fzf @x` splatted a string character-by-character and fzf saw
a bare '-' → "unknown option: -" (exit 2, no UI). The 3-element --query branch stayed
an array, so only the common empty-buffer case broke. Use literal fzf args, no splat.
Also bind Ctrl+r into both Vi key-tables (-ViMode Insert/Command) with a FzfHistory
brief-description: EditMode is Vi, and a plain bind lands only in the Insert table,
leaving Command-mode Ctrl+r on the Phase-1 ReverseSearchHistory bind. Now Ctrl+r is
fzf history in either Vi mode, and the duplicate Get-PSReadLineKeyHandler row is gone.
Vendor psmux-sidebar (psmux/psmux-plugins@0f46ccc, the same pinned rev as resurrect/continuum): Prefix+Tab toggles a directory-tree pane, Prefix+Shift+Tab focuses it. Stateless — static scripts/, PPM loader never run, source-file'd directly. setup.ps1's generic plugin-copy loop picks it up with no installer edit. Inline the useful psmux-pain-control keybinds (it ships only bindings, no scripts): - Prefix+Alt+h/j/k/l repeatable vim-key pane resize (5 cells) - Prefix+</> repeatable window reorder - Prefix+| \ (side-by-side) and - _ (top/bottom) mnemonic split aliases, inherit cwd Skipped its Prefix+hjkl nav (redundant with unified Ctrl-hjkl) and Prefix+c (already bound). Docs updated: psmux/README.md, psmux/plugins/README.md, CLAUDE.md.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds a psmux (native-Windows tmux clone;
winget install marlocarlo.psmux) module for one-key pane equalize (select-layout tiled) — the thing zellij has no native action for. zellij stays installed as a fallback (retirement trigger documented inCLAUDE.md).Highlights
after-split-windowhook.Prefix+gtoggles live; status bar shows[grid]when armed.Ctrl-hjklnav across psmux panes + nvim splits. nvim needs no change — psmux sets$TMUX, so the existingkeymaps.luaedge-handoff works.sensibledefaults inlined assetlines (no theme/sensible plugins).Enter-/Remove-/Get-PsmuxSession) + native tab-completion in the pwsh profile (Phase 2b).psmux-resurrect+psmux-continuum.Security (this is the load-bearing part)
A
/councilreview flagged a namespace-hijack RCE: PPM (psmux's plugin manager) resolves@plugin 'psmux-plugins/<name>'by cloninggithub.com/psmux-plugins/<name>.gitfirst — an unregistered, attacker-registrable org (HTTP 404) — then runs it at startup. Anyone could register that org and get code-exec on install.Fix: PPM is bypassed entirely. The two persistence plugins are vendored + SHA-pinned (
psmux-plugins@0f46ccca) and loaded viasource-fileof their staticplugin.conf— no resolver, no@plugin, norun, no network, noPrefix+I/U/M. (psmux/plugins/is excluded from the PSScriptAnalyzer gate as vendored third-party.)Verification
tests/psmux.Tests.ps1that loads the real config end-to-end and asserts the persistence binds).Still needs a live check (can't be tested headlessly)
Commits
chore(psmux): vendor the two plugins (pinned).feat(psmux): the module, config, profile helpers, tests, CI, docs.