muxr 0.1.8 → 0.1.10

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 16281336febc64c008a43dcd0419fb9ea796dc49f9a36b9ac16f2e20373f302e
4
- data.tar.gz: d28305f5833ecd082ab850e981a074b1c936d0a06829fbfa1be8814d67ed4faa
3
+ metadata.gz: 5b49dc8c773efbfdca08e50511ca4c66b7ef9026d5873e7f0d0f6f9adb24f946
4
+ data.tar.gz: 1d9db1a9dd14bfeb6e84646c91ff2c0804bff406d9e7716bf59f6b0dd2a4cbba
5
5
  SHA512:
6
- metadata.gz: a31cf649a48541e25b417ab92f9ec14cda5d64acfd0827532792d181effc6b051d5c97234f6fe734ffc97c951819caf857da43a2902ec7068d4f0a5b798c1e40
7
- data.tar.gz: 22b1e5990231d697960c9fe8ef7e7d8c4a6502eb8305497dc5759b8cd5dab785f38f21be92ab835a69afd194b74f1eaf45b184332b64cb5c1d7e97545e937ba1
6
+ metadata.gz: 3147be5a9ab44a49eb9df20d69ea39a91218f8df5d182de13f141f1dcac2cc3f43ddf157dc1ef8f6088f1852aeeef130ce3b835476f317e2a41b8f331ac5b068
7
+ data.tar.gz: 847059de7300b33d2b9c84ab46d5505ca15b70211a9f1588418c8112ba4c20ac495f7964a7ea181e2246a3e9542acb26df149bad0097164332f8eb629cfc1a19
data/CHANGELOG.md CHANGED
@@ -6,6 +6,38 @@ follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.10] - 2026-05-29
10
+
11
+ ### Added
12
+ - Six new layouts join `tall`, `grid`, and `monocle`:
13
+ - **`wide`** (`w`) — master on top, slaves split across the bottom.
14
+ - **`columns`** (`|`) — equal-width, full-height vertical strips.
15
+ - **`rows`** (`-`) — equal-height, full-width horizontal strips.
16
+ - **`spiral`** (`f`) — Fibonacci spiral winding inward, each pane half
17
+ the size of the last.
18
+ - **`centered`** (`e`) — master in a centred column with slaves dealt
19
+ to both sides.
20
+ - **`stack`** (`S`) — accordion: the focused pane expands while the
21
+ others collapse to title slivers.
22
+ The `:layout` command resolves any unambiguous name prefix across all
23
+ nine layouts; `C-a Tab` / `Tab` cycles through them in order. README
24
+ screenshots cover every layout.
25
+
26
+ ## [0.1.9] - 2026-05-29
27
+
28
+ ### Added
29
+ - `:layout` accepts short-form prefixes — `:layout t` / `g` / `m` map to
30
+ tall / grid / monocle via prefix matching. Full names still work; an
31
+ ambiguous prefix flashes the candidate layouts.
32
+ - README screenshots are now generated by [VHS](https://github.com/charmbracelet/vhs)
33
+ tapes under `docs/screenshots/tapes/`, regenerated with a single
34
+ `regenerate.sh` run. New captures cover scrollback `/` search and
35
+ movable-cursor visual selection.
36
+
37
+ ### Documentation
38
+ - Documented that wrapped plain-text URLs are stamped with OSC 8
39
+ hyperlink ids (the 0.1.8 feature) in the README architecture section.
40
+
9
41
  ## [0.1.8] - 2026-05-22
10
42
 
11
43
  ### Added
data/README.md CHANGED
@@ -33,17 +33,49 @@ with the border color — tracks the current [input mode](#modes).
33
33
 
34
34
  ## Screenshots
35
35
 
36
- The three built-in layouts (pick directly with `t`/`g`/`m` in normal mode, or cycle with `Tab` / `C-a Tab`):
36
+ The built-in layouts (pick directly with the keys below in normal mode, or cycle with `Tab` / `C-a Tab`):
37
+
38
+ | Layout | Key | Geometry |
39
+ |--------|-----|----------|
40
+ | `tall` | `t` | master on the left, slaves stacked on the right |
41
+ | `wide` | `w` | master on top, slaves split across the bottom |
42
+ | `columns` | `\|` | equal-width full-height vertical strips |
43
+ | `rows` | `-` | equal-height full-width horizontal strips |
44
+ | `grid` | `g` | roughly-square even tiling |
45
+ | `spiral` | `f` | Fibonacci spiral winding inward (each pane half the last) |
46
+ | `centered` | `e` | master in a centred column, slaves dealt to both sides |
47
+ | `stack` | `S` | accordion — focused pane expands, others collapse to title slivers |
48
+ | `monocle` | `m` | focused pane fullscreen |
37
49
 
38
50
  <table>
39
51
  <tr>
40
52
  <td align="center"><strong>tall</strong><br/>master + stacked slaves</td>
41
- <td align="center"><strong>grid</strong><br/>even tiling</td>
42
- <td align="center"><strong>monocle</strong><br/>focused pane fullscreen</td>
53
+ <td align="center"><strong>wide</strong><br/>master on top, slaves below</td>
54
+ <td align="center"><strong>columns</strong><br/>equal-width strips</td>
43
55
  </tr>
44
56
  <tr>
45
57
  <td><img src="docs/screenshots/01-layout-tall.png" alt="tall layout"></td>
58
+ <td><img src="docs/screenshots/05-layout-wide.png" alt="wide layout"></td>
59
+ <td><img src="docs/screenshots/06-layout-columns.png" alt="columns layout"></td>
60
+ </tr>
61
+ <tr>
62
+ <td align="center"><strong>rows</strong><br/>equal-height strips</td>
63
+ <td align="center"><strong>grid</strong><br/>even tiling</td>
64
+ <td align="center"><strong>spiral</strong><br/>Fibonacci spiral</td>
65
+ </tr>
66
+ <tr>
67
+ <td><img src="docs/screenshots/07-layout-rows.png" alt="rows layout"></td>
46
68
  <td><img src="docs/screenshots/02-layout-grid.png" alt="grid layout"></td>
69
+ <td><img src="docs/screenshots/08-layout-spiral.png" alt="spiral layout"></td>
70
+ </tr>
71
+ <tr>
72
+ <td align="center"><strong>centered</strong><br/>master flanked by slaves</td>
73
+ <td align="center"><strong>stack</strong><br/>accordion of title slivers</td>
74
+ <td align="center"><strong>monocle</strong><br/>focused pane fullscreen</td>
75
+ </tr>
76
+ <tr>
77
+ <td><img src="docs/screenshots/09-layout-centered.png" alt="centered layout"></td>
78
+ <td><img src="docs/screenshots/10-layout-stack.png" alt="stack layout"></td>
47
79
  <td><img src="docs/screenshots/03-layout-monocle.png" alt="monocle layout"></td>
48
80
  </tr>
49
81
  </table>
@@ -52,6 +84,17 @@ The Quake-style drawer overlay (`~` in normal mode, `C-a ~` in passthrough):
52
84
 
53
85
  ![drawer overlay](docs/screenshots/04-drawer.png)
54
86
 
87
+ Scrollback / copy-mode (`s`) with `/` search — matches highlight in
88
+ yellow and the focused pane border turns orange:
89
+
90
+ ![scrollback search](docs/screenshots/05-scrollback-search.png)
91
+
92
+ Movable-cursor visual selection (`v` inside scrollback) — the border
93
+ turns magenta and the swept region is highlighted, ready to yank with
94
+ `y`:
95
+
96
+ ![visual selection](docs/screenshots/06-selection.png)
97
+
55
98
  ## Install / run
56
99
 
57
100
  ```bash
@@ -89,7 +132,7 @@ muxr has two top-level input modes, modeled on vim:
89
132
 
90
133
  - **Normal** (default at startup) — single keys act on the multiplexer.
91
134
  `hjkl` moves focus between panes, `HJKL` moves the focused pane
92
- itself, `c`/`x` create/close panes, `t`/`g`/`m` set the layout, etc.
135
+ itself, `c`/`x` create/close panes, `t`/`w`/`g`/`m` (and `|`/`-`/`f`/`e`/`S`) set the layout, etc.
93
136
  No prefix needed.
94
137
  - **Passthrough** (entered with `i`) — every keystroke is forwarded to
95
138
  the focused pane, exactly like a regular terminal. muxr commands are
@@ -113,7 +156,8 @@ regardless of mode.
113
156
  | `H` / `J` / `K` / `L`| move focused pane left / down / up / right |
114
157
  | `i` | drop into passthrough mode |
115
158
  | `c` / `x` | new / close focused pane (close asks `y/n`) |
116
- | `t` / `g` / `m` | layout: tall / grid / monocle |
159
+ | `t` / `w` / `g` / `m`| layout: tall / wide / grid / monocle |
160
+ | `\|` / `-` / `f` / `e` / `S` | layout: columns / rows / spiral / centered / stack |
117
161
  | `Tab` / `Enter` | cycle layout / promote focused to master |
118
162
  | `a` / `1` … `9` | toggle last pane / jump to pane by number |
119
163
  | `s` | enter scrollback / copy-mode |
@@ -144,7 +188,7 @@ the move falls back to linear next/prev shuffling.
144
188
  | `C-a a` | toggle last (previously focused) pane |
145
189
  | `C-a 1` … `9` | jump to pane by its label |
146
190
  | `C-a x` | close focused pane (asks `y/n`; hides drawer with no prompt) |
147
- | `C-a Tab` | cycle layout (`tall` → `grid` → `monocle`) |
191
+ | `C-a Tab` | cycle layout (`tall` → `wide` → `columns` → `rows` → `grid` → `spiral` → `centered` → `stack` → `monocle`) |
148
192
  | `C-a Enter` | promote focused pane to master |
149
193
  | `C-a ~` | toggle drawer (shell) |
150
194
  | `C-a C` | toggle Claude Code drawer (MCP-aware) |
@@ -206,7 +250,10 @@ focused pane.
206
250
  ## Commands (typed after `:` in normal mode, or `C-a :` in passthrough)
207
251
 
208
252
  ```
209
- layout {tall|grid|monocle} # also: layout (no arg) → cycle
253
+ layout {tall|wide|columns|rows|grid|spiral|centered|stack|monocle}
254
+ # any unambiguous name prefix works (t, w, r, g, m, …);
255
+ # ambiguous ones (c → columns/centered, s → spiral/stack)
256
+ # flash the candidates. layout (no arg) → cycle
210
257
  drawer {toggle|show|hide|reset}
211
258
  claude # toggle the Claude Code drawer
212
259
  private # toggle private flag on focused pane
@@ -324,7 +371,11 @@ The per-pane `Terminal` is a real VT100 emulator (cursor movement, SGR
324
371
  including 256-color/truecolor and underline subparameters, erase/insert/
325
372
  delete, autowrap, scroll regions). Scrollback is composited into the
326
373
  visible grid through a view-offset that auto-tracks new rows while
327
- scrolled back, so reviewed content stays frozen.
374
+ scrolled back, so reviewed content stays frozen. Plain-text `http`/
375
+ `https`/`ftp` URLs that wrap across rows are re-stamped with matching
376
+ OSC 8 hyperlink ids after each feed, so terminals like Ghostty, iTerm2,
377
+ kitty, and WezTerm merge the wrapped halves back into one clickable
378
+ link (program-emitted OSC 8 payloads are left untouched).
328
379
 
329
380
  ## Session persistence
330
381
 
@@ -390,6 +441,22 @@ On-disk layout:
390
441
  └─ logs/<name>.log server stdout/stderr
391
442
  ```
392
443
 
444
+ ### Regenerating the README screenshots
445
+
446
+ The PNGs under `docs/screenshots/` are produced by [`vhs`](https://github.com/charmbracelet/vhs)
447
+ driving muxr itself — one `.tape` file per screenshot under
448
+ `docs/screenshots/tapes/`. After a UI change, refresh them with:
449
+
450
+ ```bash
451
+ brew install vhs # one-time
452
+ docs/screenshots/tapes/regenerate.sh # renders all six
453
+ ```
454
+
455
+ Each tape spawns a throwaway `shot` session, populates one or more panes
456
+ with `ls`/`git log`/`wc` output, drives the feature being shown (layout,
457
+ drawer, scrollback search, selection), and writes a single PNG via
458
+ `Screenshot`. Tweak the tape if the keybindings or status bar change.
459
+
393
460
  ## Contributing
394
461
 
395
462
  Contributions are welcome from anyone, with one requirement: **the code
@@ -45,10 +45,12 @@ module Muxr
45
45
  @app.cycle_layout
46
46
  return
47
47
  end
48
- sym = name.to_sym
49
- if Window::LAYOUTS.include?(sym)
50
- @app.session.window.set_layout(sym)
48
+ matches = Window::LAYOUTS.select { |l| l.to_s.start_with?(name) }
49
+ if matches.length == 1
50
+ @app.session.window.set_layout(matches.first)
51
51
  @app.invalidate
52
+ elsif matches.length > 1
53
+ @app.flash("ambiguous layout: #{name} (#{matches.join(", ")})")
52
54
  else
53
55
  @app.flash("unknown layout: #{name}")
54
56
  end
@@ -31,8 +31,14 @@ module Muxr
31
31
  "c" => :new_pane,
32
32
  "x" => :request_close,
33
33
  "t" => [:set_layout, :tall],
34
+ "w" => [:set_layout, :wide],
34
35
  "g" => [:set_layout, :grid],
35
36
  "m" => [:set_layout, :monocle],
37
+ "|" => [:set_layout, :columns],
38
+ "-" => [:set_layout, :rows],
39
+ "f" => [:set_layout, :spiral],
40
+ "e" => [:set_layout, :centered],
41
+ "S" => [:set_layout, :stack],
36
42
  "\t" => :cycle_layout,
37
43
  "\r" => :promote_master,
38
44
  "\n" => :promote_master,
@@ -10,7 +10,7 @@ module Muxr
10
10
  end
11
11
  end
12
12
 
13
- LAYOUTS = %i[tall grid monocle].freeze
13
+ LAYOUTS = %i[tall wide columns rows grid spiral centered stack monocle].freeze
14
14
 
15
15
  module_function
16
16
 
@@ -19,9 +19,15 @@ module Muxr
19
19
  master_index = master_index.clamp(0, count - 1)
20
20
  focused_index = focused_index.clamp(0, count - 1)
21
21
  case layout
22
- when :tall then tall(count, area, master_index)
23
- when :grid then grid(count, area)
24
- when :monocle then monocle(count, area, focused_index)
22
+ when :tall then tall(count, area, master_index)
23
+ when :wide then wide(count, area, master_index)
24
+ when :columns then columns(count, area)
25
+ when :rows then rows(count, area)
26
+ when :grid then grid(count, area)
27
+ when :spiral then spiral(count, area)
28
+ when :centered then centered(count, area, master_index)
29
+ when :stack then stack(count, area, focused_index)
30
+ when :monocle then monocle(count, area, focused_index)
25
31
  else
26
32
  raise ArgumentError, "Unknown layout: #{layout.inspect}"
27
33
  end
@@ -52,6 +58,153 @@ module Muxr
52
58
  rects
53
59
  end
54
60
 
61
+ # The transpose of `tall`: master pane spans the full width across the top
62
+ # half; remaining panes sit side-by-side in the bottom half, dividing the
63
+ # remaining width evenly.
64
+ def wide(count, area, master_index = 0)
65
+ master_index = master_index.clamp(0, count - 1)
66
+ return [Rect.new(area.x, area.y, area.w, area.h)] if count == 1
67
+
68
+ master_h = [area.h / 2, 1].max
69
+ stack_h = [area.h - master_h, 1].max
70
+ others = (0...count).to_a - [master_index]
71
+ slave_count = others.length
72
+ base_w = area.w / slave_count
73
+ remainder = area.w - base_w * slave_count
74
+
75
+ rects = Array.new(count)
76
+ rects[master_index] = Rect.new(area.x, area.y, area.w, master_h)
77
+
78
+ x = area.x
79
+ others.each_with_index do |idx, i|
80
+ w = base_w + (i < remainder ? 1 : 0)
81
+ rects[idx] = Rect.new(x, area.y + master_h, w, stack_h)
82
+ x += w
83
+ end
84
+ rects
85
+ end
86
+
87
+ # Equal-width, full-height vertical strips, side by side. No master.
88
+ def columns(count, area)
89
+ base_w = area.w / count
90
+ rem = area.w - base_w * count
91
+ rects = []
92
+ x = area.x
93
+ count.times do |i|
94
+ w = base_w + (i < rem ? 1 : 0)
95
+ rects << Rect.new(x, area.y, w, area.h)
96
+ x += w
97
+ end
98
+ rects
99
+ end
100
+
101
+ # Equal-height, full-width horizontal strips, stacked. The dual of columns.
102
+ def rows(count, area)
103
+ base_h = area.h / count
104
+ rem = area.h - base_h * count
105
+ rects = []
106
+ y = area.y
107
+ count.times do |i|
108
+ h = base_h + (i < rem ? 1 : 0)
109
+ rects << Rect.new(area.x, y, area.w, h)
110
+ y += h
111
+ end
112
+ rects
113
+ end
114
+
115
+ # Fibonacci spiral: each pane takes half of the remaining region, splitting
116
+ # vertically then horizontally in alternation, so panes wind inward toward
117
+ # the bottom-right. The last pane fills whatever is left.
118
+ def spiral(count, area)
119
+ x, y, w, h = area.x, area.y, area.w, area.h
120
+ rects = []
121
+ count.times do |i|
122
+ if i == count - 1
123
+ rects << Rect.new(x, y, w, h)
124
+ elsif i.even?
125
+ left = [w / 2, 1].max
126
+ rects << Rect.new(x, y, left, h)
127
+ x += left
128
+ w = [w - left, 1].max
129
+ else
130
+ top = [h / 2, 1].max
131
+ rects << Rect.new(x, y, w, top)
132
+ y += top
133
+ h = [h - top, 1].max
134
+ end
135
+ end
136
+ rects
137
+ end
138
+
139
+ # Three-column master: master occupies the centre column full-height; the
140
+ # remaining panes are dealt alternately to a left and a right column and
141
+ # stacked within each. With a single slave there is no symmetry to keep, so
142
+ # it falls back to a simple master/slave vertical split (like `tall`).
143
+ def centered(count, area, master_index = 0)
144
+ master_index = master_index.clamp(0, count - 1)
145
+ return [Rect.new(area.x, area.y, area.w, area.h)] if count == 1
146
+
147
+ others = (0...count).to_a - [master_index]
148
+ rects = Array.new(count)
149
+
150
+ if others.length == 1
151
+ master_w = [area.w / 2, 1].max
152
+ rects[master_index] = Rect.new(area.x, area.y, master_w, area.h)
153
+ rects[others[0]] = Rect.new(area.x + master_w, area.y, [area.w - master_w, 1].max, area.h)
154
+ return rects
155
+ end
156
+
157
+ master_w = [area.w / 2, 1].max
158
+ side_w = area.w - master_w
159
+ left_w = [side_w / 2, 1].max
160
+ right_w = [side_w - left_w, 1].max
161
+
162
+ rects[master_index] = Rect.new(area.x + left_w, area.y, master_w, area.h)
163
+ left = others.select.with_index { |_, i| i.even? }
164
+ right = others.select.with_index { |_, i| i.odd? }
165
+ stack_column(rects, left, area.x, area.y, left_w, area.h)
166
+ stack_column(rects, right, area.x + left_w + master_w, area.y, right_w, area.h)
167
+ rects
168
+ end
169
+
170
+ # Accordion: the focused pane expands to fill the leftover height while the
171
+ # others collapse to short "title sliver" rows, all stacked vertically.
172
+ # Like monocle but the other panes stay visible (and spatially reachable).
173
+ def stack(count, area, focused_index = 0)
174
+ return [Rect.new(area.x, area.y, area.w, area.h)] if count == 1
175
+ focused_index = focused_index.clamp(0, count - 1)
176
+
177
+ others = count - 1
178
+ # Sliver is 3 rows so draw_box can still render the title; shrink it only
179
+ # when the terminal is too short to give the focused pane its own 3 rows.
180
+ sliver = [3, [area.h - 3, 0].max / others].min
181
+ sliver = [sliver, 1].max
182
+ focus_h = area.h - sliver * others
183
+
184
+ rects = Array.new(count)
185
+ y = area.y
186
+ count.times do |i|
187
+ h = (i == focused_index) ? focus_h : sliver
188
+ rects[i] = Rect.new(area.x, y, area.w, h)
189
+ y += h
190
+ end
191
+ rects
192
+ end
193
+
194
+ # Stack the given pane indices vertically within a single column, dividing
195
+ # the height evenly (remainder to the topmost panes). Used by `centered`.
196
+ def stack_column(rects, indices, x, y, w, total_h)
197
+ return if indices.empty?
198
+ base_h = total_h / indices.length
199
+ rem = total_h - base_h * indices.length
200
+ cy = y
201
+ indices.each_with_index do |idx, i|
202
+ h = base_h + (i < rem ? 1 : 0)
203
+ rects[idx] = Rect.new(x, cy, w, h)
204
+ cy += h
205
+ end
206
+ end
207
+
55
208
  # Roughly square grid. Each row stretches its panes to fill the full width
56
209
  # so an underfull bottom row doesn't leave gaps.
57
210
  def grid(count, area)
data/lib/muxr/renderer.rb CHANGED
@@ -391,7 +391,8 @@ module Muxr
391
391
  " H / J / K / L move pane left / down / up / right",
392
392
  " i drop into passthrough mode",
393
393
  " c / x new / close pane (close asks y/n)",
394
- " t / g / m layout: tall / grid / monocle",
394
+ " t / w / g / m layout: tall / wide / grid / monocle",
395
+ " | - f e S layout: columns / rows / spiral / centered / stack",
395
396
  " Tab / Enter cycle layout / promote to master",
396
397
  " a / 1..9 last pane / jump by number",
397
398
  " s enter scrollback",
@@ -401,7 +402,7 @@ module Muxr
401
402
  "",
402
403
  "PASSTHROUGH mode (keys reach the focused pane; prefix is Ctrl-a)",
403
404
  " C-a Esc return to normal mode",
404
- " C-a c x t g m same as normal-mode bindings",
405
+ " C-a c x t w g m same as normal-mode bindings",
405
406
  " C-a Tab Enter cycle layout / promote master",
406
407
  " C-a n / p / a next / prev / last pane",
407
408
  " C-a [ ] scrollback / paste buffer",
@@ -413,7 +414,8 @@ module Muxr
413
414
  " cursor: h/j/k/l 0/^/$ w/e/b W/E/B H/M/L g/G",
414
415
  " v select, C-v block, y/Enter yank, q/Esc cancel",
415
416
  "",
416
- "Commands: layout {tall|grid|monocle}, drawer {toggle|show|hide|reset},",
417
+ "Commands: layout {tall|wide|columns|rows|grid|spiral|centered|stack|monocle},",
418
+ " drawer {toggle|show|hide|reset},",
417
419
  " claude, save, restore, sessions, quit, new, close, next, prev",
418
420
  "",
419
421
  "press any key to dismiss"
data/lib/muxr/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Muxr
2
- VERSION = "0.1.8"
2
+ VERSION = "0.1.10"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: muxr
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roel Bondoc