muxr 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +70 -0
- data/README.md +229 -68
- data/lib/muxr/application.rb +236 -0
- data/lib/muxr/command_dispatcher.rb +1 -1
- data/lib/muxr/foreground_command.rb +86 -0
- data/lib/muxr/input_handler.rb +256 -27
- data/lib/muxr/layout_manager.rb +59 -0
- data/lib/muxr/pane.rb +10 -0
- data/lib/muxr/renderer.rb +193 -43
- data/lib/muxr/terminal.rb +195 -2
- data/lib/muxr/version.rb +1 -1
- data/lib/muxr/window.rb +13 -0
- data/lib/muxr.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 03650b7088c85aa8fbe993dbfe17e7f8aac8334f617c3ff148589964d83bb701
|
|
4
|
+
data.tar.gz: 9b878beb1c05f1c83273f3f1c8e51944be7c75b3fdcd13935c96f851a2aa8405
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 91ccd01254428f3a2333e3abe94cd3d039598813c484730a4819c938f9f0d733ab611bd9c975314ca5bfb0af07c243c69814ed918fc02315e7652436454170b8
|
|
7
|
+
data.tar.gz: c58966f41d4a62978e3bfd7560d72ef132b6fcbdededddf129cf94216d77689b1f1f52c44cf7660cab9780a7ec8f3180fe138a9f2f75f8ec741548595c415665
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,76 @@ follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.7] - 2026-05-20
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Scrollback search via `/` (forward) and `?` (backward). Enter commits
|
|
13
|
+
the query, `n` / `N` cycle through matches with wrap. Smart-case
|
|
14
|
+
matching scans both scrollback and the live buffer; the chosen match
|
|
15
|
+
is centered in the viewport and every match is highlighted in yellow
|
|
16
|
+
until the user exits scrollback.
|
|
17
|
+
- Arrow / page keys in scrollback. `↑`/`↓` scroll a line, `PgUp`/`PgDn`
|
|
18
|
+
half-page, `Home`/`End` jump to top/bottom. `InputHandler#feed` now
|
|
19
|
+
peeks for CSI escape sequences so a bare `Esc` still exits scrollback
|
|
20
|
+
the way it always has.
|
|
21
|
+
- Shift-`H`/`J`/`K`/`L` swaps the focused pane with the spatial
|
|
22
|
+
neighbor in that direction (linear next/prev fallback in monocle).
|
|
23
|
+
Focus tracks the moved pane so you can keep dragging it; swapping
|
|
24
|
+
into position 0 of tall/grid promotes it to master.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- Pane close is now confirmed. The close binding moved from `K` to
|
|
28
|
+
`x` (in both normal mode and the Ctrl-a prefix) so shift-`HJKL` is
|
|
29
|
+
free for the new move action. Close routes through a `:confirm_close`
|
|
30
|
+
state that flashes `close pane? (y/n)` — drawer hide stays
|
|
31
|
+
prompt-free since it's reversible.
|
|
32
|
+
|
|
33
|
+
## [0.1.6] - 2026-05-15
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
- Vim-style `:normal` mode as the default startup state. Single keys
|
|
37
|
+
(hjkl, c, K, t/g/m, s, etc.) act directly without the Ctrl-a prefix;
|
|
38
|
+
`i` drops into the historical `:passthrough` mode and `Ctrl-a Esc`
|
|
39
|
+
returns to normal.
|
|
40
|
+
- Spatial hjkl navigation via a new `LayoutManager.neighbor` that
|
|
41
|
+
computes pane adjacency from layout rects, so focus moves the way the
|
|
42
|
+
eye expects in tall and grid layouts (monocle falls back to linear).
|
|
43
|
+
- `[MODE]` chip rendered in the top-right corner of the focused
|
|
44
|
+
container, plus a per-mode color palette on the focused pane border
|
|
45
|
+
and status chip (cyan normal, green passthrough, orange scrollback,
|
|
46
|
+
magenta selection, yellow command, red quit-confirm, blue help). The
|
|
47
|
+
`:prefix` substate shares the passthrough green so the border doesn't
|
|
48
|
+
flicker when Ctrl-a is pressed.
|
|
49
|
+
- Foreground-command annotation in pane titles. A 750ms background
|
|
50
|
+
poller (`Application#start_foreground_poller`) walks each pane's
|
|
51
|
+
foreground process group and stamps the name onto
|
|
52
|
+
`pane.foreground_command`; titles now read `#1 abc123 · npm test`
|
|
53
|
+
when the shell isn't itself in the foreground. Lookup goes through
|
|
54
|
+
`/proc/<pid>/stat` on Linux and `ps` on macOS, off the event loop.
|
|
55
|
+
- OSC 8 hyperlink passthrough. The Terminal buffers OSC payloads,
|
|
56
|
+
extracts OSC 8 link bodies (interned per terminal), and stamps the
|
|
57
|
+
active link onto every cell. The Renderer carries hyperlink through
|
|
58
|
+
its Cell struct and wraps each contiguous run with one open/close
|
|
59
|
+
pair, so Ghostty et al. treat a wrapped URL as one clickable link.
|
|
60
|
+
- Space as a thumb-friendly alias for `v` in selection mode (toggle
|
|
61
|
+
linear anchor).
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
- Focused pane title now shows the mode label
|
|
65
|
+
(`[NORMAL]`/`[PASS]`/etc.) instead of the redundant layout name —
|
|
66
|
+
the status bar already shows the active layout.
|
|
67
|
+
- `[MODE]` chip moved out of the title and into the top-right corner
|
|
68
|
+
of the focused container, freeing the left-side title for the
|
|
69
|
+
foreground command.
|
|
70
|
+
- Dropped the old `space → page-down` mapping in scrollback so space
|
|
71
|
+
is available for the new selection-mode alias.
|
|
72
|
+
|
|
73
|
+
### Fixed
|
|
74
|
+
- Exiting scrollback now restores the previous base mode (normal or
|
|
75
|
+
passthrough) instead of always landing in normal. A
|
|
76
|
+
passthrough → scrollback → exit round-trip now leaves you back in
|
|
77
|
+
passthrough.
|
|
78
|
+
|
|
9
79
|
## [0.1.5] - 2026-05-13
|
|
10
80
|
|
|
11
81
|
### Added
|
data/README.md
CHANGED
|
@@ -7,22 +7,33 @@ like tiling-window-manager clients — you never resize them by hand;
|
|
|
7
7
|
the active layout decides geometry.
|
|
8
8
|
|
|
9
9
|
```
|
|
10
|
-
|
|
11
|
-
│ master pane
|
|
12
|
-
│
|
|
13
|
-
│
|
|
14
|
-
│
|
|
15
|
-
│
|
|
16
|
-
|
|
17
|
-
┌ Drawer
|
|
18
|
-
│ persistent overlay shell, opens from the bottom
|
|
19
|
-
|
|
20
|
-
[default] panes:3 layout:tall focused:#1 drawer:shown
|
|
10
|
+
┌─ #1 a3f9b2 ★ · npm test ──── [NORMAL] ─┬─ #2 c2e810 ──────────────┐
|
|
11
|
+
│ master pane (running npm test) │ stacked slave pane │
|
|
12
|
+
│ │ │
|
|
13
|
+
│ ├──────────────────────────┤
|
|
14
|
+
│ │ #3 9b1d04 [P] │
|
|
15
|
+
│ │ private pane (MCP-hidden)│
|
|
16
|
+
└────────────────────────────────────────┴──────────────────────────┘
|
|
17
|
+
┌ Drawer ────────────────────────────────────────────────────────────┐
|
|
18
|
+
│ persistent overlay shell, opens from the bottom │
|
|
19
|
+
└────────────────────────────────────────────────────────────────────┘
|
|
20
|
+
[NORMAL] [default] panes:3 layout:tall focused:#1 drawer:shown muxr ^a ?
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
Each pane shows its slot (`#1`, `#2`, …) plus a stable 6-hex id
|
|
24
|
+
(`a3f9b2`). The slot is positional and shifts when panes are created,
|
|
25
|
+
killed, or promoted; the id is generated once and survives layout
|
|
26
|
+
changes, detach/reattach, and cold-restart from the session JSON. `[P]`
|
|
27
|
+
marks a private pane that the MCP control surface refuses to read or
|
|
28
|
+
drive (see [MCP control surface](#mcp-control-surface) below). The
|
|
29
|
+
focused pane's title shows the foreground command running in its PTY
|
|
30
|
+
(e.g. `· npm test`) when something other than the shell is in the
|
|
31
|
+
foreground, and the `[NORMAL]` chip in the top-right corner — along
|
|
32
|
+
with the border color — tracks the current [input mode](#modes).
|
|
33
|
+
|
|
23
34
|
## Screenshots
|
|
24
35
|
|
|
25
|
-
The three built-in layouts (cycle with `C-a Tab`):
|
|
36
|
+
The three built-in layouts (pick directly with `t`/`g`/`m` in normal mode, or cycle with `Tab` / `C-a Tab`):
|
|
26
37
|
|
|
27
38
|
<table>
|
|
28
39
|
<tr>
|
|
@@ -37,42 +48,107 @@ The three built-in layouts (cycle with `C-a Tab`):
|
|
|
37
48
|
</tr>
|
|
38
49
|
</table>
|
|
39
50
|
|
|
40
|
-
The Quake-style drawer overlay (`C-a ~`):
|
|
51
|
+
The Quake-style drawer overlay (`~` in normal mode, `C-a ~` in passthrough):
|
|
41
52
|
|
|
42
53
|

|
|
43
54
|
|
|
44
55
|
## Install / run
|
|
45
56
|
|
|
46
57
|
```bash
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
gem install muxr
|
|
59
|
+
muxr # attach the "default" session (auto-spawn if needed)
|
|
60
|
+
muxr work # attach (or start) a named session
|
|
61
|
+
muxr --list # list running sessions and exit
|
|
62
|
+
muxr --install-skill # install the MCP skill into ~/.claude/skills
|
|
63
|
+
muxr --help
|
|
53
64
|
```
|
|
54
65
|
|
|
55
66
|
Requires **Ruby ≥ 3.4**. No runtime gems — just `PTY`, `IO.console`, `JSON`,
|
|
56
67
|
`Socket`, and `FileUtils` from stdlib.
|
|
57
68
|
|
|
58
|
-
`
|
|
69
|
+
`muxr` is the client. The first invocation for a session daemonizes a
|
|
59
70
|
server in the background; subsequent invocations attach to it over a Unix
|
|
60
|
-
socket. `
|
|
61
|
-
shell it owns) running, so reattaching
|
|
62
|
-
with their full history.
|
|
71
|
+
socket. `d` (normal mode) / `C-a d` (passthrough) detaches the client
|
|
72
|
+
and leaves the server (and every shell it owns) running, so reattaching
|
|
73
|
+
gives you back the exact same panes with their full history.
|
|
63
74
|
|
|
64
|
-
|
|
75
|
+
### From source
|
|
76
|
+
|
|
77
|
+
To run the latest unreleased code or hack on muxr locally, clone the repo
|
|
78
|
+
and use `bin/muxr` directly — it puts `lib/` on `$LOAD_PATH` itself:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
git clone https://github.com/roelbondoc/muxr
|
|
82
|
+
cd muxr
|
|
83
|
+
bin/muxr # same flags as the installed `muxr` executable
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Modes
|
|
87
|
+
|
|
88
|
+
muxr has two top-level input modes, modeled on vim:
|
|
89
|
+
|
|
90
|
+
- **Normal** (default at startup) — single keys act on the multiplexer.
|
|
91
|
+
`hjkl` moves focus between panes, `HJKL` moves the focused pane
|
|
92
|
+
itself, `c`/`x` create/close panes, `t`/`g`/`m` set the layout, etc.
|
|
93
|
+
No prefix needed.
|
|
94
|
+
- **Passthrough** (entered with `i`) — every keystroke is forwarded to
|
|
95
|
+
the focused pane, exactly like a regular terminal. muxr commands are
|
|
96
|
+
reached via the historical `Ctrl-a` prefix. `Ctrl-a Esc` returns to
|
|
97
|
+
normal mode.
|
|
98
|
+
|
|
99
|
+
The active mode appears as a `[MODE]` chip in the top-right corner of
|
|
100
|
+
the focused pane (and the leftmost slot of the status bar). The
|
|
101
|
+
focused pane's border is colored by mode — cyan for normal, green for
|
|
102
|
+
passthrough, orange for scrollback (and its `/` search prompt),
|
|
103
|
+
magenta for selection, yellow for the command prompt, red during the
|
|
104
|
+
kill-session or close-pane confirmation, blue while help is open.
|
|
105
|
+
Unfocused panes always render with the grey unfocused border,
|
|
106
|
+
regardless of mode.
|
|
107
|
+
|
|
108
|
+
### Normal mode
|
|
109
|
+
|
|
110
|
+
| Keys | Action |
|
|
111
|
+
|----------------------|-----------------------------------------------------|
|
|
112
|
+
| `h` / `j` / `k` / `l`| focus pane left / down / up / right (spatial) |
|
|
113
|
+
| `H` / `J` / `K` / `L`| move focused pane left / down / up / right |
|
|
114
|
+
| `i` | drop into passthrough mode |
|
|
115
|
+
| `c` / `x` | new / close focused pane (close asks `y/n`) |
|
|
116
|
+
| `t` / `g` / `m` | layout: tall / grid / monocle |
|
|
117
|
+
| `Tab` / `Enter` | cycle layout / promote focused to master |
|
|
118
|
+
| `a` / `1` … `9` | toggle last pane / jump to pane by number |
|
|
119
|
+
| `s` | enter scrollback / copy-mode |
|
|
120
|
+
| `~` / `C` / `P` | drawer / Claude drawer / toggle private flag |
|
|
121
|
+
| `]` | paste internal yank buffer into focused pane |
|
|
122
|
+
| `:` / `?` | command prompt / help |
|
|
123
|
+
| `d` / `q` | detach / kill session (asks `y/n`) |
|
|
124
|
+
|
|
125
|
+
`h`/`j`/`k`/`l` does true spatial navigation — it inspects the current
|
|
126
|
+
layout's rectangles and picks the closest neighbor in the requested
|
|
127
|
+
direction. In monocle (where every pane occupies the full area) it
|
|
128
|
+
falls back to linear next/previous so the keys still do something
|
|
129
|
+
useful.
|
|
130
|
+
|
|
131
|
+
`H`/`J`/`K`/`L` swap the focused pane with the spatial neighbor in
|
|
132
|
+
that direction, then keep focus on the moved pane so you can keep
|
|
133
|
+
dragging it. Swapping into position 0 of `tall`/`grid` promotes the
|
|
134
|
+
pane to master (the layout master is always `panes[0]`). In monocle
|
|
135
|
+
the move falls back to linear next/prev shuffling.
|
|
136
|
+
|
|
137
|
+
### Passthrough mode (`Ctrl-a` prefix)
|
|
65
138
|
|
|
66
139
|
| Keys | Action |
|
|
67
140
|
|----------------|---------------------------------------------------------|
|
|
141
|
+
| `C-a Esc` | return to normal mode |
|
|
68
142
|
| `C-a c` | new pane |
|
|
69
|
-
| `C-a n` / `p` | focus next / previous pane
|
|
143
|
+
| `C-a n` / `p` | focus next / previous pane (linear) |
|
|
70
144
|
| `C-a a` | toggle last (previously focused) pane |
|
|
71
145
|
| `C-a 1` … `9` | jump to pane by its label |
|
|
72
|
-
| `C-a
|
|
146
|
+
| `C-a x` | close focused pane (asks `y/n`; hides drawer with no prompt) |
|
|
73
147
|
| `C-a Tab` | cycle layout (`tall` → `grid` → `monocle`) |
|
|
74
148
|
| `C-a Enter` | promote focused pane to master |
|
|
75
|
-
| `C-a ~` | toggle drawer
|
|
149
|
+
| `C-a ~` | toggle drawer (shell) |
|
|
150
|
+
| `C-a C` | toggle Claude Code drawer (MCP-aware) |
|
|
151
|
+
| `C-a P` | toggle private flag on focused pane (hides from MCP) |
|
|
76
152
|
| `C-a [` | enter scrollback / copy-mode |
|
|
77
153
|
| `C-a ]` | paste internal yank buffer into focused pane |
|
|
78
154
|
| `C-a d` | detach (server keeps running) |
|
|
@@ -83,17 +159,25 @@ with their full history.
|
|
|
83
159
|
|
|
84
160
|
### Scrollback and copy-mode
|
|
85
161
|
|
|
86
|
-
Each pane keeps a bounded (5000-row) scrollback ring. `
|
|
87
|
-
|
|
88
|
-
the pane title gains
|
|
162
|
+
Each pane keeps a bounded (5000-row) scrollback ring. `s` in normal
|
|
163
|
+
mode (or `C-a [` in passthrough) enters scrollback with vi-style
|
|
164
|
+
navigation; the status bar shows a key hint and the pane title gains
|
|
165
|
+
`[scrollback N/M]`.
|
|
89
166
|
|
|
90
167
|
| Keys | Action |
|
|
91
168
|
|-------------------------|-------------------------------------|
|
|
92
|
-
| `j` / `k`
|
|
93
|
-
| `d` / `u` (or `C-d`/`C-u`) | half page
|
|
169
|
+
| `j` / `k` or `↓` / `↑` | scroll one line |
|
|
170
|
+
| `d` / `u` (or `C-d`/`C-u`, `PgDn`/`PgUp`) | half page |
|
|
94
171
|
| `f` / Space (or `C-f`/`C-b`) | full page |
|
|
95
|
-
| `g` / `G`
|
|
96
|
-
|
|
|
172
|
+
| `g` / `G` (or `Home` / `End`) | top / bottom |
|
|
173
|
+
| `/` *query* `Enter` | search forward (toward newer); `?` searches backward |
|
|
174
|
+
| `n` / `N` | next / previous match in the search direction (wraps) |
|
|
175
|
+
| `q` / `Esc` / `C-c` | exit back to normal mode |
|
|
176
|
+
|
|
177
|
+
Search uses smart-case (case-insensitive unless the query has an
|
|
178
|
+
uppercase letter), scans both scrollback and the live buffer, and
|
|
179
|
+
centers the chosen match in the viewport. Matches stay highlighted in
|
|
180
|
+
yellow while you're in scrollback; exiting clears the highlight.
|
|
97
181
|
|
|
98
182
|
Press `v` inside scrollback to enter a movable-cursor selection mode.
|
|
99
183
|
Vim-style motions are supported:
|
|
@@ -109,21 +193,23 @@ Vim-style motions are supported:
|
|
|
109
193
|
| `H` / `M` / `L` | top / middle / bottom of viewport |
|
|
110
194
|
| `C-d`/`C-u`, `C-f`/`C-b`, Space | half / full page |
|
|
111
195
|
| `v` / `C-v` | anchor char / block selection (toggle) |
|
|
112
|
-
| `y` or Enter | yank and
|
|
196
|
+
| `y` or Enter | yank and return to normal mode |
|
|
113
197
|
| `q` / `Esc` / `C-c` | cancel back to scrollback |
|
|
114
198
|
|
|
115
199
|
`v` and `C-v` toggle between character and block (rectangular) selection
|
|
116
200
|
— switching between the two preserves the anchor. `y` or Enter yanks the
|
|
117
201
|
selection into an internal buffer, pipes it to `pbcopy` in the background
|
|
118
|
-
(silent no-op when `pbcopy` is unavailable), and
|
|
119
|
-
|
|
120
|
-
pane.
|
|
202
|
+
(silent no-op when `pbcopy` is unavailable), and returns to normal mode.
|
|
203
|
+
`]` (normal) / `C-a ]` (passthrough) writes the yank buffer back into the
|
|
204
|
+
focused pane.
|
|
121
205
|
|
|
122
|
-
## Commands (typed after `C-a :`)
|
|
206
|
+
## Commands (typed after `:` in normal mode, or `C-a :` in passthrough)
|
|
123
207
|
|
|
124
208
|
```
|
|
125
209
|
layout {tall|grid|monocle} # also: layout (no arg) → cycle
|
|
126
210
|
drawer {toggle|show|hide|reset}
|
|
211
|
+
claude # toggle the Claude Code drawer
|
|
212
|
+
private # toggle private flag on focused pane
|
|
127
213
|
save # persist session to ~/.muxr/sessions/<name>.json
|
|
128
214
|
restore # show path to saved session
|
|
129
215
|
sessions | ls # list saved sessions
|
|
@@ -131,6 +217,58 @@ new | close | next | prev | master
|
|
|
131
217
|
detach | quit # quit asks for y/n confirmation
|
|
132
218
|
```
|
|
133
219
|
|
|
220
|
+
## MCP control surface
|
|
221
|
+
|
|
222
|
+
muxr exposes a second listener at `~/.muxr/sockets/<name>.ctrl.sock`
|
|
223
|
+
that accepts multiple concurrent NDJSON clients over a small JSON-RPC
|
|
224
|
+
surface (`session.get`, `panes.list`, `pane.read`, `pane.send_input`,
|
|
225
|
+
`pane.run`, `pane.subscribe`, `pane.kill`, `layout.set`, `drawer.*`,
|
|
226
|
+
…). The control socket is independent of TTY attach — programmatic
|
|
227
|
+
clients never count as "attached", so a Claude Code session and a human
|
|
228
|
+
can drive the multiplexer concurrently.
|
|
229
|
+
|
|
230
|
+
`pane.run` waits for the PTY to go idle before responding: it sends the
|
|
231
|
+
input, polls for output, and returns once no bytes have arrived for
|
|
232
|
+
`idle_ms` (default 500). Server-side idle detection avoids the
|
|
233
|
+
send-then-poll race that plagues naive client-side automation.
|
|
234
|
+
|
|
235
|
+
`pane.send_input`, `pane.run`, and `drawer.send_input` accept a `keys`
|
|
236
|
+
array of vim-style `<name>` tokens (`<esc>`, `<c-c>`, `<cr>`, arrows,
|
|
237
|
+
etc.) interleaved with literal text — callers don't have to remember
|
|
238
|
+
that Escape is `"\e"` and Ctrl-C is `"\x03"`. Bracketed-paste wrapping
|
|
239
|
+
still applies to literal segments only.
|
|
240
|
+
|
|
241
|
+
### Claude Code integration
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
muxr --install-skill # copies skills/muxr-control into ~/.claude/skills
|
|
245
|
+
# and prints the `claude mcp add` registration line
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`bin/muxr-mcp` is the standalone MCP-over-stdio bridge that translates
|
|
249
|
+
Claude Code tool calls into NDJSON requests on the control socket. It
|
|
250
|
+
auto-detects the target session from `MUXR_CONTROL_SOCKET` or
|
|
251
|
+
`MUXR_SESSION` env vars.
|
|
252
|
+
|
|
253
|
+
`C` (normal) / `C-a C` (passthrough) / `:claude` opens a drawer whose shell is `claude`, with
|
|
254
|
+
`MUXR_SESSION`, `MUXR_CONTROL_SOCKET`, `MUXR_FOCUSED_PANE`, and
|
|
255
|
+
`MUXR_DRAWER_SELF=1` injected into its environment. The bridge picks
|
|
256
|
+
those up automatically; you get a Quake-style Claude Code overlay that
|
|
257
|
+
already knows what session it's in. `MUXR_DRAWER_SELF` makes the bridge
|
|
258
|
+
refuse `drawer.*` methods, so a claude drawer can't recurse into its
|
|
259
|
+
own PTY.
|
|
260
|
+
|
|
261
|
+
### Private panes
|
|
262
|
+
|
|
263
|
+
`P` (normal) / `C-a P` (passthrough) / `:private` flips the private flag on the focused pane.
|
|
264
|
+
Private panes are hidden from programmatic callers: `panes.list` strips
|
|
265
|
+
cwd/rows/cols, and `pane.read`, `pane.send_input`, `pane.run`,
|
|
266
|
+
`pane.subscribe`, and `pane.kill` refuse with an error message pointing
|
|
267
|
+
the human at the TTY (`P` / `C-a P`) to expose it. The flag is persisted in session
|
|
268
|
+
JSON and shown as `[P]` in the pane title bar. The MCP surface
|
|
269
|
+
intentionally has no method to flip the flag — only a human at the TTY
|
|
270
|
+
can mark a pane public again.
|
|
271
|
+
|
|
134
272
|
## Architecture
|
|
135
273
|
|
|
136
274
|
muxr runs as **two processes** that talk over a Unix domain socket at
|
|
@@ -144,26 +282,37 @@ Client (foreground, owns the TTY) Server (daemon, owns the PTYs)
|
|
|
144
282
|
├─ SIGWINCH → RESIZE frame ├─ Session ─ Window ─ Pane[ ] ─ Terminal + PTYProcess
|
|
145
283
|
│ │ └─ Drawer ─ Pane
|
|
146
284
|
└─ Protocol ├─ Renderer – diff-emits ANSI as OUTPUT frames
|
|
147
|
-
◄── OUTPUT bytes ──── Renderer ◄────────────┤ InputHandler –
|
|
285
|
+
◄── OUTPUT bytes ──── Renderer ◄────────────┤ InputHandler – normal/passthrough mode state machine
|
|
148
286
|
──── INPUT bytes ───► InputHandler ├─ CommandDispatcher – parses ":"-prefixed commands
|
|
149
287
|
──── HELLO/RESIZE ──► apply_size ├─ LayoutManager – pure (layout, count, area) → [Rect]
|
|
150
|
-
◄── BYE ───────────── disconnect_client
|
|
288
|
+
◄── BYE ───────────── disconnect_client ├─ UNIXServer (TTY socket, one client at a time)
|
|
289
|
+
└─ UNIXServer (.ctrl.sock, many NDJSON clients)
|
|
151
290
|
```
|
|
152
291
|
|
|
153
292
|
Frames are length-prefixed (`[1-byte type][4-byte BE length][payload]`):
|
|
154
293
|
`H` hello, `I` input, `R` resize, `B` bye, `O` output.
|
|
155
294
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
295
|
+
A second listener at `~/.muxr/sockets/<name>.ctrl.sock` accepts
|
|
296
|
+
multiple concurrent NDJSON clients for the MCP control surface (see
|
|
297
|
+
above). The two sockets are independent — programmatic clients never
|
|
298
|
+
count as "attached", so they don't lock out the human's TTY client.
|
|
299
|
+
|
|
300
|
+
The server's event loop is single-threaded `IO.select` over the
|
|
301
|
+
listening sockets, the attached client (when present), every pane PTY,
|
|
302
|
+
the drawer PTY, and every connected control client. A single
|
|
303
|
+
background thread polls each pane's foreground process group every
|
|
304
|
+
750ms (`/proc/<pid>/stat` on Linux, `ps -o tpgid=,pgid=` on macOS) so
|
|
305
|
+
the `· cmd` annotation in the pane title can refresh without blocking
|
|
306
|
+
the render loop. Everything else stays on the main thread. Layouts
|
|
307
|
+
are pure — `LayoutManager` has no mutable state, so the renderer
|
|
308
|
+
recomputes geometry on every tick after a resize or pane add/remove
|
|
309
|
+
without bookkeeping.
|
|
310
|
+
|
|
311
|
+
`d` (normal) / `C-a d` (passthrough) detaches the client but leaves
|
|
312
|
+
the server (and its shells) running; reattaching gives you back the
|
|
313
|
+
same panes with their full history. `q` / `C-a q` / `:quit` flash
|
|
314
|
+
`kill session? (y/n)` in the status bar and only tear the server down
|
|
315
|
+
on `y` — there is no "kill without confirm" keybinding by design.
|
|
167
316
|
|
|
168
317
|
The drawer's PTY is **never torn down** when the drawer is hidden — its
|
|
169
318
|
shell process keeps running so the next toggle restores the previous
|
|
@@ -187,16 +336,24 @@ Sessions live in `~/.muxr/sessions/<name>.json`:
|
|
|
187
336
|
"layout": "tall",
|
|
188
337
|
"focused_index": 0,
|
|
189
338
|
"master_index": 0,
|
|
190
|
-
"panes": [
|
|
339
|
+
"panes": [
|
|
340
|
+
{"id": "a3f9b2", "cwd": "/home/me/code", "private": false},
|
|
341
|
+
{"id": "c2e810", "cwd": "/tmp", "private": true}
|
|
342
|
+
],
|
|
191
343
|
"drawer": {"visible": true, "cwd": "/home/me/code"}
|
|
192
344
|
}
|
|
193
345
|
```
|
|
194
346
|
|
|
347
|
+
Pane ids and the private flag are persisted, so the same ids survive
|
|
348
|
+
cold-restart from the JSON snapshot and a pane that was marked private
|
|
349
|
+
stays private.
|
|
350
|
+
|
|
195
351
|
The JSON file is mainly a **cold-storage fallback**. Between detaches the
|
|
196
|
-
live session lives inside the running server process, so `
|
|
197
|
-
`bin/muxr <name>` reattaches to the exact
|
|
198
|
-
history. The JSON only matters once the
|
|
199
|
-
or a reboot): re-launching
|
|
352
|
+
live session lives inside the running server process, so `d` (normal) /
|
|
353
|
+
`C-a d` (passthrough) then `bin/muxr <name>` reattaches to the exact
|
|
354
|
+
same shells with their full history. The JSON only matters once the
|
|
355
|
+
server is gone (after `q` / `C-a q` or a reboot): re-launching
|
|
356
|
+
`muxr <name>` rebuilds pane and drawer shells
|
|
200
357
|
using the saved working directories. Shell command history within those
|
|
201
358
|
panes is **not** persisted — that's the job of your shell's own history
|
|
202
359
|
file. Run `:save` from inside muxr to write the snapshot.
|
|
@@ -205,28 +362,32 @@ file. Run `:save` from inside muxr to write the snapshot.
|
|
|
205
362
|
|
|
206
363
|
```bash
|
|
207
364
|
bundle install # only minitest and rake
|
|
208
|
-
rake test # full suite (
|
|
365
|
+
rake test # full suite (200+ unit tests)
|
|
209
366
|
|
|
210
367
|
# Run a single file or test
|
|
211
368
|
ruby -Ilib -Itest test/test_layout_manager.rb
|
|
212
369
|
ruby -Ilib -Itest test/test_terminal.rb -n test_csi_cursor_position
|
|
213
370
|
```
|
|
214
371
|
|
|
215
|
-
Tests cover the layout algorithms
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
372
|
+
Tests cover the layout algorithms (including spatial neighbor lookup
|
|
373
|
+
for `hjkl`), drawer state machine, window pane ordering, session JSON
|
|
374
|
+
round-trip, the client/server framing protocol, the input-handler
|
|
375
|
+
state machine (normal/passthrough mode transitions, scrollback,
|
|
376
|
+
selection), foreground-command parsing (Linux `/proc` stat format and
|
|
377
|
+
shell-filter rules), the renderer's diff-emit, and the VT100
|
|
378
|
+
emulator's cursor movement, SGR (including colon-subparameter and
|
|
379
|
+
underline-color forms), erase, scroll-region, and autowrap handling.
|
|
380
|
+
PTY-dependent code paths are exercised via dependency injection so
|
|
381
|
+
tests don't spawn shells.
|
|
222
382
|
|
|
223
383
|
On-disk layout:
|
|
224
384
|
|
|
225
385
|
```
|
|
226
386
|
~/.muxr/
|
|
227
|
-
├─ sessions/<name>.json
|
|
228
|
-
├─ sockets/<name>.sock
|
|
229
|
-
|
|
387
|
+
├─ sessions/<name>.json structural snapshot written by `:save`
|
|
388
|
+
├─ sockets/<name>.sock TTY client listener (auto-managed)
|
|
389
|
+
├─ sockets/<name>.ctrl.sock MCP control listener (auto-managed)
|
|
390
|
+
└─ logs/<name>.log server stdout/stderr
|
|
230
391
|
```
|
|
231
392
|
|
|
232
393
|
## Contributing
|