muxr 0.1.11 → 0.2.0
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 +225 -1
- data/README.md +763 -387
- data/bin/muxr +68 -33
- data/bin/muxr-mcp +172 -16
- data/lib/muxr/application.rb +516 -26
- data/lib/muxr/client.rb +26 -4
- data/lib/muxr/command_dispatcher.rb +69 -0
- data/lib/muxr/config.rb +156 -0
- data/lib/muxr/control_server.rb +301 -3
- data/lib/muxr/history_row.rb +264 -0
- data/lib/muxr/image_store.rb +61 -0
- data/lib/muxr/input_handler.rb +112 -14
- data/lib/muxr/layout_manager.rb +83 -57
- data/lib/muxr/mouse_report.rb +24 -0
- data/lib/muxr/pane.rb +122 -2
- data/lib/muxr/pane_picker.rb +49 -0
- data/lib/muxr/pane_transfer.rb +172 -0
- data/lib/muxr/protocol.rb +51 -5
- data/lib/muxr/pty_process.rb +34 -7
- data/lib/muxr/remote_pane.rb +263 -0
- data/lib/muxr/renderer.rb +259 -76
- data/lib/muxr/session.rb +19 -2
- data/lib/muxr/session_directory.rb +102 -0
- data/lib/muxr/terminal.rb +759 -82
- data/lib/muxr/version.rb +1 -1
- data/lib/muxr/width_probe.rb +121 -0
- data/lib/muxr/window.rb +50 -3
- data/lib/muxr.rb +8 -0
- data/muxr.gemspec +10 -0
- data/skills/muxr-control/SKILL.md +158 -8
- metadata +18 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f207f0ebd4d74ae0660e3659cd9ad9703ff414266621d5a2624c5771d267cd0c
|
|
4
|
+
data.tar.gz: f8302e2987a759d5efe1c5971cd9845815f529c2dd901d0eaf229b6e4d107888
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f746020a1a69c8a90fb96cde030357eb9ac999b1b99e8f88a3b3d23b92d9047bcecc0830edddf4b897fc8627029ee3f539f82bc9216712a3ebff76248ab052aa
|
|
7
|
+
data.tar.gz: 633ff78595a4fc0f041decea8f4a574321908bc7d3a8624e087b000d55b84487baf870f5c9b3ad3f1db5fd517f2f712ff1bd79e33b1763df951801acd1c6c60b
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,229 @@ follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.0] - 2026-09-23
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- A flash is visible again in the modes that actually raise one. The status
|
|
13
|
+
bar's message branch was the last `elsif` in a chain starting with the
|
|
14
|
+
command prompt and running through scrollback, search and selection, so any
|
|
15
|
+
mode with a full-row overlay painted the row and the message never drew.
|
|
16
|
+
"not found: <query>" from a failed search and "yanked N bytes" from a
|
|
17
|
+
selection both land back in scrollback, where neither had ever been seen;
|
|
18
|
+
the scroll-source work added four more that only fire there, which is why
|
|
19
|
+
`Tab` on a pane with no scroll of its own read as a dead key rather than a
|
|
20
|
+
refusal with a reason. The message now draws after the chain, right-aligned
|
|
21
|
+
over the overlay — transient and specific beats a static key hint.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- `:capture [path]` saves the focused pane's full scrollback and screen to a
|
|
25
|
+
plain-text file: no escape codes, trailing spaces trimmed, wide glyphs
|
|
26
|
+
intact. With no path it writes to
|
|
27
|
+
`~/.muxr/captures/<session>-<pane>-<timestamp>.txt`. A relative path is
|
|
28
|
+
resolved against the directory the session was started in. While a
|
|
29
|
+
full-screen program is up, the capture holds the shell underneath rather
|
|
30
|
+
than the program's frame.
|
|
31
|
+
- A config file at `~/.muxr/config.json` (or wherever `MUXR_CONFIG`
|
|
32
|
+
points) sets `layout`, `scrollback`, `master_ratio`, `master_count`,
|
|
33
|
+
`auto_spiral_min` (`{"cols": 180, "rows": 30}`), `prefix` (`"C-b"` to use
|
|
34
|
+
Ctrl-b instead of Ctrl-a), and `keys`, which remaps normal-mode and
|
|
35
|
+
prefix-mode keys onto existing actions (`{"normal": {"Z": "toggle_zoom",
|
|
36
|
+
"q": null}}`; `null` unbinds a key). Settings that don't make sense are
|
|
37
|
+
skipped and reported: the first one is flashed when you attach, and all of
|
|
38
|
+
them go to the session log. A saved session's layout still wins over the
|
|
39
|
+
configured default, and `MUXR_SCROLLBACK` still wins over `scrollback`.
|
|
40
|
+
`:reload` re-reads the file without restarting the server. A key can also
|
|
41
|
+
run any prompt command: `{"normal": {"Y": ":sync"}}`. A configured prefix
|
|
42
|
+
shows up everywhere muxr names the prefix: the help overlay, the `muxr ^b ?`
|
|
43
|
+
hint in the status bar, the `[^B]` chip while the prefix is pending, the
|
|
44
|
+
passthrough flash, and `muxr --help` (which also says which config file the
|
|
45
|
+
prefix came from).
|
|
46
|
+
- `:rename <name>` gives the focused pane a label, shown in its title in
|
|
47
|
+
place of the hex id (`#1 api`). A bare `:rename` clears it. Names are saved
|
|
48
|
+
with the session, travel with a pane when it is moved to another session,
|
|
49
|
+
appear in the attach picker, and are listed by `muxr_panes_list` (except
|
|
50
|
+
for private panes). Any MCP tool that takes `pane` now also accepts a name.
|
|
51
|
+
An id always wins over a name that happens to look like one, and a name
|
|
52
|
+
shared by two panes is refused. The bridge's refusal to let a claude read
|
|
53
|
+
or type into its own pane also applies when that pane is referred to by
|
|
54
|
+
name.
|
|
55
|
+
- `:sync` sends what you type to every pane in the window at once. It works
|
|
56
|
+
in passthrough mode, and `C-a ]` pastes go to every pane too. Use it to run
|
|
57
|
+
the same command on several hosts or checkouts. `:sync on` / `:sync off`
|
|
58
|
+
set it explicitly, and a bare `:sync` toggles it. While it is on, the
|
|
59
|
+
status bar shows a red `[SYNC]` and every unfocused pane gets a red border,
|
|
60
|
+
so you cannot forget it is on. Borrowed panes are included, and your
|
|
61
|
+
keystrokes reach their real shells. The drawer is left out in both
|
|
62
|
+
directions: typing in the drawer stays in the drawer. Sync is deliberately
|
|
63
|
+
not saved with the session.
|
|
64
|
+
- `z` (or `C-a z`, or `:zoom`) zooms the focused pane to full screen, and
|
|
65
|
+
pressing it again restores the layout you were in. The status bar reads
|
|
66
|
+
`layout:zoom:tall` while zoomed, so you can see what `z` will return to.
|
|
67
|
+
Picking any layout explicitly forgets the zoom. A session saved while
|
|
68
|
+
zoomed saves the layout underneath.
|
|
69
|
+
- The master area can be resized and can hold more than one pane, the way
|
|
70
|
+
it can in xmonad. `<` / `>` shrink or grow the master's share of the
|
|
71
|
+
screen in 5% steps, between 10% and 90%. `,` / `.` take a pane out of the
|
|
72
|
+
master area or add one to it, so two panes can share the master column.
|
|
73
|
+
Both keys work in normal mode and after `C-a`, and both apply to `tall`,
|
|
74
|
+
`wide` and `centered`. `:ratio 60` and `:masters 2` set them directly. The
|
|
75
|
+
current shape is flashed on every change and saved with the session.
|
|
76
|
+
- `:silence <secs>` alerts you when the focused pane stops printing. Once
|
|
77
|
+
the pane has been quiet for that long, muxr flashes `pane #2 silent for
|
|
78
|
+
30s`, rings the outer terminal's bell, and marks the pane `~` in its title
|
|
79
|
+
and in `alerts:`. It fires once per quiet spell and re-arms on the next
|
|
80
|
+
output. It takes `30`, `30s` or `2m`. `:silence off` disarms it, and a bare
|
|
81
|
+
`:silence` reports the current setting. An armed pane shows
|
|
82
|
+
`[silence 30s]` in its title, and the threshold is saved with the session.
|
|
83
|
+
- Panes that want your attention are marked. A pane that rings the bell or
|
|
84
|
+
sends an OSC 9 / OSC 777 notification while you are looking elsewhere gets
|
|
85
|
+
a `!` after its number in the title (`#2!`), and one that prints anything
|
|
86
|
+
gets a `•` (`#3•`). The status bar collects them as `alerts:2!,3•`. Both
|
|
87
|
+
clear the moment the pane is focused. The pane you are looking at is never
|
|
88
|
+
marked, unless no client is attached, in which case nobody is looking at
|
|
89
|
+
it either. Output within 1.5 s of a pane being resized or created is not
|
|
90
|
+
counted: every layout change sends SIGWINCH to every shell, and each one
|
|
91
|
+
answers by redrawing its prompt, which would otherwise light up every pane
|
|
92
|
+
on screen.
|
|
93
|
+
- Scrollback can now drive the program instead of muxr's ring. A pane whose
|
|
94
|
+
program has turned on mouse tracking (DECSET 1000/1002/1003 — `lazygit`,
|
|
95
|
+
`k9s`, `htop`) gets `C-a [` routed to *it*: muxr synthesises
|
|
96
|
+
wheel reports at the centre of the pane and writes them into the pty, so
|
|
97
|
+
`j`/`k`, `C-d`/`C-u` and `C-f`/`C-b` scroll the program's own transcript
|
|
98
|
+
from the keyboard, no mouse involved. SGR encoding when the program asked
|
|
99
|
+
for 1006, legacy X10 otherwise. A full-screen program that wants no mouse
|
|
100
|
+
falls back to arrow keys, which is what `less` and `vim` respond to — so
|
|
101
|
+
`C-a [` no longer dead-ends with "no history while a full-screen app is
|
|
102
|
+
running" on an alternate-screen pane. `Tab` switches between the app's
|
|
103
|
+
scroll and muxr's ring at any time, and the mode chip reads `SCROLL:APP`
|
|
104
|
+
while the app has it. `Terminal` tracks the mouse modes and carries them in
|
|
105
|
+
`dump_ansi`, so a mirrored or moved pane knows its program speaks mouse.
|
|
106
|
+
Visual selection is clamped to the visible screen while the app is being
|
|
107
|
+
scrolled: a self-scrolling program repaints in place, so the ring's tail no
|
|
108
|
+
longer continues into row 0 and a selection spanning that seam would splice
|
|
109
|
+
two unrelated regions into the paste buffer.
|
|
110
|
+
- The MCP bridge now reaches *any* claude started inside muxr, not just the
|
|
111
|
+
one in the Claude drawer. `MUXR_SESSION` and `MUXR_CONTROL_SOCKET` are
|
|
112
|
+
injected into every PTY muxr spawns, so with the bridge registered once at
|
|
113
|
+
user scope (`claude mcp add muxr muxr-mcp --scope user`) a claude launched
|
|
114
|
+
from an ordinary pane is already wired to the session around it. Each
|
|
115
|
+
pane's shell also carries `MUXR_PANE`, its own id: the bridge refuses
|
|
116
|
+
`pane.read`, `pane.send_input`, `pane.run` and `pane.kill` against that
|
|
117
|
+
pane, since driving your own pty feeds your output back to you — the pane
|
|
118
|
+
equivalent of the drawer's `MUXR_DRAWER_SELF` guard. Panes that predate the
|
|
119
|
+
upgrade need reopening to pick the vars up.
|
|
120
|
+
- The bridge follows its pane across a move. Those env vars are a snapshot of
|
|
121
|
+
where the pane was when its shell started, and moving a pane to another
|
|
122
|
+
session leaves them naming the old one — a running process's environment
|
|
123
|
+
cannot be rewritten from outside. When the session the env names no longer
|
|
124
|
+
lists `MUXR_PANE` among its own panes, the bridge asks the other control
|
|
125
|
+
sockets in the same directory which of them owns it and connects there
|
|
126
|
+
instead, re-checking on a ten-second TTL so a pane moved out from under a
|
|
127
|
+
running claude is followed rather than left driving its old session. A
|
|
128
|
+
mirrored pane carries `origin` and is never mistaken for the owner.
|
|
129
|
+
- Attach a pane from another muxr session: `A` (or `C-a A`, or `:attach`)
|
|
130
|
+
opens a picker listing every pane the other live servers on this machine
|
|
131
|
+
are willing to share, grouped by session. The chosen pane is *shared, not
|
|
132
|
+
moved* — it keeps running where it is, both sessions show the same live
|
|
133
|
+
shell, and either can type into it. The owner keeps the PTY and remains
|
|
134
|
+
its only reader; what crosses the control socket is the raw byte stream
|
|
135
|
+
and keystrokes, so colors, the alternate screen and full-screen TUIs all
|
|
136
|
+
mirror faithfully. The PTY runs at the smallest viewport looking at it so
|
|
137
|
+
it fits both layouts. If the owning session stops, the borrower drops the
|
|
138
|
+
pane; if the borrower stops, only the mirror goes away and the pane
|
|
139
|
+
carries on at home. Private panes are never offered, and borrowed panes
|
|
140
|
+
are left out of `:save`. New control methods: `pane.mirror`,
|
|
141
|
+
`pane.mirror_resize`, `pane.unmirror`, `pane.redraw`; `pane.send_input`
|
|
142
|
+
and `pane.run` now also accept `base64: true` payloads.
|
|
143
|
+
- Move a pane between sessions for real, with `m` in the same picker. The
|
|
144
|
+
master pty file descriptor crosses the socket via `SCM_RIGHTS`, so the
|
|
145
|
+
same shell process keeps running — environment, background jobs, screen
|
|
146
|
+
and full scrollback intact — and simply belongs to the receiving session
|
|
147
|
+
afterwards, as an ordinary local pane. The handoff is two-phase: nothing
|
|
148
|
+
is torn down until the receiver confirms it has a working pane, a failure
|
|
149
|
+
anywhere leaves the pane where it was, and an abandoned move times out
|
|
150
|
+
after ten seconds instead of pausing the pane forever. muxr refuses to
|
|
151
|
+
move the last pane out of a session (it would shut that session down) or
|
|
152
|
+
to move on a pane that is itself borrowed. Sessions mirroring a pane that
|
|
153
|
+
moves away are told it is gone and detach. New control methods:
|
|
154
|
+
`pane.move`, `pane.move_commit`, `pane.move_abort`.
|
|
155
|
+
- Kitty graphics protocol images are saved to `~/.muxr/images` and
|
|
156
|
+
announced in the pane as a clickable `file://` path (OSC 8) instead of
|
|
157
|
+
being drawn. muxr renders a cell grid and has nowhere to put pixels, so
|
|
158
|
+
`[image 640×480 → ~/.muxr/images/…png]` is the honest rendering —
|
|
159
|
+
Cmd-click opens it. Multi-chunk (`m=1`) transmissions are reassembled,
|
|
160
|
+
`t=f`/`t=t` file transmissions are read from disk, and raw `f=24`/`f=32`
|
|
161
|
+
pixel data is re-encoded into a PNG container so the saved file is
|
|
162
|
+
actually openable. A capability query (`a=q`) is answered `OK` so inner
|
|
163
|
+
programs pick the kitty path instead of falling back to sixel. The store
|
|
164
|
+
keeps the 200 most recent images.
|
|
165
|
+
- A project page at https://roelbondoc.github.io/muxr/, served from `docs/`.
|
|
166
|
+
Its layout playground runs a port of `LayoutManager` in the browser, so
|
|
167
|
+
pressing muxr's own layout and `hjkl` keys tiles with the real algorithm.
|
|
168
|
+
A **Screen** control switches the simulated terminal between 132×38 and
|
|
169
|
+
220×54 so `auto` can be seen changing its mind.
|
|
170
|
+
- An `auto` layout (`F`, or `:layout auto`) that picks its geometry from the
|
|
171
|
+
screen it is on: `spiral` once the content area is at least 180×30,
|
|
172
|
+
`stack` below that. It is a resolver rather than a tenth geometry —
|
|
173
|
+
`LayoutManager.resolve` maps it to a real layout and `compute` resolves
|
|
174
|
+
before dispatching, so nothing downstream sees `:auto`. No new size
|
|
175
|
+
plumbing: SIGWINCH already reaches the server as a `RESIZE` frame, so
|
|
176
|
+
detaching onto a laptop or plugging in a monitor re-resolves on the next
|
|
177
|
+
frame. The status bar names the verdict, e.g. `layout:auto:stack`.
|
|
178
|
+
|
|
179
|
+
- The alternate screen buffer (DECSET 47/1047/1049, and 1048 for its cursor
|
|
180
|
+
slot). A full-screen program — a pager, an editor, `fzf` — now draws on a
|
|
181
|
+
grid of its own, and the screen it covered is set aside untouched until it
|
|
182
|
+
exits. Nothing drawn there reaches scrollback and the view offset is pinned
|
|
183
|
+
to the live screen, so there is nothing to page into; scrollback declines to
|
|
184
|
+
open on such a pane, and a pane that enters an alternate screen under a
|
|
185
|
+
reader drops them out of the mode. `Terminal#dump_ansi` leads with a repaint
|
|
186
|
+
of the covered screen, so a mirrored or moved pane carries both grids and
|
|
187
|
+
quitting the pager on the far side uncovers the same shell it uncovers at
|
|
188
|
+
home.
|
|
189
|
+
- `MUXR_SCROLLBACK` sets the scrollback depth at server start, clamped to a
|
|
190
|
+
sane range.
|
|
191
|
+
|
|
192
|
+
### Changed
|
|
193
|
+
- `muxr-mcp` no longer exits when it can't find a muxr session. Registering
|
|
194
|
+
it at user scope loads it into every claude on the machine, most of which
|
|
195
|
+
are nowhere near a muxr, and exiting surfaced a failed MCP server in all of
|
|
196
|
+
them. It now completes the handshake, advertises no tools when there is no
|
|
197
|
+
session to talk to, and says so if called anyway. The socket is opened on
|
|
198
|
+
first use rather than at startup and reopened if it drops, so a claude
|
|
199
|
+
running in a pane survives a restart of the session around it.
|
|
200
|
+
- `auto` is the default layout for new windows (was `spiral`). Saved
|
|
201
|
+
sessions are unaffected.
|
|
202
|
+
- Scrollback holds 50,000 rows per pane by default, up from 5,000. Rows are
|
|
203
|
+
now stored packed — one string of characters plus run-length attributes,
|
|
204
|
+
trimmed to their content and materialized into cells only when read — which
|
|
205
|
+
cut a filled ring from ~330 MB of resident memory per pane to ~12 MB at
|
|
206
|
+
20,000 rows and made the deeper default affordable. Search and the transfer
|
|
207
|
+
serializer read the packed bytes directly, so `/` over a full 50,000-row
|
|
208
|
+
ring completes in tens of milliseconds.
|
|
209
|
+
|
|
210
|
+
### Fixed
|
|
211
|
+
- The help overlay claimed `C-a t w g m` set layouts and `muxr --help`
|
|
212
|
+
claimed `C-a k` closed a pane. Neither binding exists: layout keys are
|
|
213
|
+
normal-mode only, and close is `C-a x`.
|
|
214
|
+
- `--list` and `:sessions` no longer report a phantom `<name>.ctrl`
|
|
215
|
+
session. Both enumerated `~/.muxr/sockets/*.sock`, which also matches
|
|
216
|
+
the sibling control socket `<name>.ctrl.sock`.
|
|
217
|
+
- A pane whose grid is larger than the box drawn for it is now clipped to
|
|
218
|
+
the box instead of painting over its own border. Only reachable with a
|
|
219
|
+
borrowed pane, whose geometry belongs to another session.
|
|
220
|
+
- Paging through a full-screen program no longer destroys scrollback. Without
|
|
221
|
+
an alternate screen buffer, a pager drew straight onto the primary grid and
|
|
222
|
+
every page-down pushed a screenful of its own frames into history, evicting
|
|
223
|
+
real output at the ring's cap — twenty page-downs of `less` cost 232 rows —
|
|
224
|
+
and quitting left the program's last frame on screen instead of uncovering
|
|
225
|
+
the shell.
|
|
226
|
+
- APC (`ESC _ … ST`) and DCS/SOS/PM (`ESC P`/`ESC X`/`ESC ^`) string
|
|
227
|
+
sequences are now consumed by the parser. Previously neither had a
|
|
228
|
+
parser state: the introducer was swallowed and the entire body printed
|
|
229
|
+
into the grid as text, so any program emitting kitty graphics or sixel
|
|
230
|
+
sprayed kilobytes of base64 across the pane.
|
|
231
|
+
|
|
9
232
|
## [0.1.11] - 2026-06-11
|
|
10
233
|
|
|
11
234
|
### Added
|
|
@@ -332,7 +555,8 @@ Initial release.
|
|
|
332
555
|
boundaries.
|
|
333
556
|
- Renderer that composes one frame and diff-emits ANSI to STDOUT.
|
|
334
557
|
|
|
335
|
-
[Unreleased]: https://github.com/roelbondoc/muxr/compare/v0.
|
|
558
|
+
[Unreleased]: https://github.com/roelbondoc/muxr/compare/v0.2.0...HEAD
|
|
559
|
+
[0.2.0]: https://github.com/roelbondoc/muxr/releases/tag/v0.2.0
|
|
336
560
|
[0.1.11]: https://github.com/roelbondoc/muxr/releases/tag/v0.1.11
|
|
337
561
|
[0.1.10]: https://github.com/roelbondoc/muxr/releases/tag/v0.1.10
|
|
338
562
|
[0.1.9]: https://github.com/roelbondoc/muxr/releases/tag/v0.1.9
|