muxr 0.1.1 → 0.1.2
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 +18 -1
- data/README.md +45 -8
- data/lib/muxr/application.rb +22 -3
- data/lib/muxr/input_handler.rb +11 -1
- data/lib/muxr/renderer.rb +4 -2
- data/lib/muxr/terminal.rb +131 -1
- data/lib/muxr/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ab53a383e7781f83ff106e503047cf5952b12193d0d65e2b29631f2beda6bcf5
|
|
4
|
+
data.tar.gz: 0fcef17549ae1745d76fe530a97f12740c9556704bbe724f1b4690932c7cabc5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4440cb89e4295407dce42ca26c474c64441ff556cb285769b085e0474448132085d9bb6ad9af91699e6349a915c90fe6ebd33825d7ee68f315cafc26ccac5244
|
|
7
|
+
data.tar.gz: e17c2f092dcd76ca617bfbfd6acdf1e5475ea9fd7afe0a7458539af95eaa32b0282d499a094f19a6887b6fd228f03d470d5a8c6b9b167b9838e6989edd20ee74
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,22 @@ follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.2] - 2026-05-11
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Vim-style word and viewport motions in copy-mode selection cursor:
|
|
13
|
+
`w`/`W`/`e`/`E`/`b`/`B` walk word and WORD boundaries, `^` jumps to
|
|
14
|
+
the first non-blank on the line, and `H`/`M`/`L` land on the visible
|
|
15
|
+
top/middle/bottom rows. Yanking now drops straight back to the live
|
|
16
|
+
shell (matching vim's `v…y` returning to normal mode); the tmux-style
|
|
17
|
+
`b` alias for page-back is now `Ctrl-b` only.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- Honor SGR 2 (dim) so faint text actually renders faint. The emulator
|
|
21
|
+
was silently dropping the attribute, which left Claude Code's
|
|
22
|
+
suggested-prompt placeholder rendering at normal intensity. SGR 22
|
|
23
|
+
now correctly clears both bold and dim per spec.
|
|
24
|
+
|
|
9
25
|
## [0.1.1] - 2026-05-11
|
|
10
26
|
|
|
11
27
|
### Fixed
|
|
@@ -51,6 +67,7 @@ Initial release.
|
|
|
51
67
|
boundaries.
|
|
52
68
|
- Renderer that composes one frame and diff-emits ANSI to STDOUT.
|
|
53
69
|
|
|
54
|
-
[Unreleased]: https://github.com/roelbondoc/muxr/compare/v0.1.
|
|
70
|
+
[Unreleased]: https://github.com/roelbondoc/muxr/compare/v0.1.2...HEAD
|
|
71
|
+
[0.1.2]: https://github.com/roelbondoc/muxr/releases/tag/v0.1.2
|
|
55
72
|
[0.1.1]: https://github.com/roelbondoc/muxr/releases/tag/v0.1.1
|
|
56
73
|
[0.1.0]: https://github.com/roelbondoc/muxr/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -20,6 +20,27 @@ the active layout decides geometry.
|
|
|
20
20
|
[default] panes:3 layout:tall focused:#1 drawer:shown muxr ^a ?
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
## Screenshots
|
|
24
|
+
|
|
25
|
+
The three built-in layouts (cycle with `C-a Tab`):
|
|
26
|
+
|
|
27
|
+
<table>
|
|
28
|
+
<tr>
|
|
29
|
+
<td align="center"><strong>tall</strong><br/>master + stacked slaves</td>
|
|
30
|
+
<td align="center"><strong>grid</strong><br/>even tiling</td>
|
|
31
|
+
<td align="center"><strong>monocle</strong><br/>focused pane fullscreen</td>
|
|
32
|
+
</tr>
|
|
33
|
+
<tr>
|
|
34
|
+
<td><img src="docs/screenshots/01-layout-tall.png" alt="tall layout"></td>
|
|
35
|
+
<td><img src="docs/screenshots/02-layout-grid.png" alt="grid layout"></td>
|
|
36
|
+
<td><img src="docs/screenshots/03-layout-monocle.png" alt="monocle layout"></td>
|
|
37
|
+
</tr>
|
|
38
|
+
</table>
|
|
39
|
+
|
|
40
|
+
The Quake-style drawer overlay (`C-a ~`):
|
|
41
|
+
|
|
42
|
+

|
|
43
|
+
|
|
23
44
|
## Install / run
|
|
24
45
|
|
|
25
46
|
```bash
|
|
@@ -70,17 +91,33 @@ the pane title gains `[scrollback N/M]`.
|
|
|
70
91
|
|-------------------------|-------------------------------------|
|
|
71
92
|
| `j` / `k` | scroll one line |
|
|
72
93
|
| `d` / `u` (or `C-d`/`C-u`) | half page |
|
|
73
|
-
| `f` /
|
|
94
|
+
| `f` / Space (or `C-f`/`C-b`) | full page |
|
|
74
95
|
| `g` / `G` | top / bottom |
|
|
75
96
|
| `q` / `Esc` / `C-c` | exit back to live view |
|
|
76
97
|
|
|
77
|
-
Press `v` inside scrollback to enter a movable-cursor selection mode
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
`
|
|
83
|
-
|
|
98
|
+
Press `v` inside scrollback to enter a movable-cursor selection mode.
|
|
99
|
+
Vim-style motions are supported:
|
|
100
|
+
|
|
101
|
+
| Keys | Action |
|
|
102
|
+
|-------------------------|-------------------------------------|
|
|
103
|
+
| `h` / `j` / `k` / `l` | left / down / up / right |
|
|
104
|
+
| `0` / `^` / `$` | line start / first non-blank / line end |
|
|
105
|
+
| `w` / `W` | next word / WORD start |
|
|
106
|
+
| `e` / `E` | next word / WORD end |
|
|
107
|
+
| `b` / `B` | previous word / WORD start |
|
|
108
|
+
| `g` / `G` | top / bottom of timeline |
|
|
109
|
+
| `H` / `M` / `L` | top / middle / bottom of viewport |
|
|
110
|
+
| `C-d`/`C-u`, `C-f`/`C-b`, Space | half / full page |
|
|
111
|
+
| `v` / `C-v` | anchor char / block selection (toggle) |
|
|
112
|
+
| `y` or Enter | yank and exit to live shell |
|
|
113
|
+
| `q` / `Esc` / `C-c` | cancel back to scrollback |
|
|
114
|
+
|
|
115
|
+
`v` and `C-v` toggle between character and block (rectangular) selection
|
|
116
|
+
— switching between the two preserves the anchor. `y` or Enter yanks the
|
|
117
|
+
selection into an internal buffer, pipes it to `pbcopy` in the background
|
|
118
|
+
(silent no-op when `pbcopy` is unavailable), and drops you straight back
|
|
119
|
+
to the live shell. `C-a ]` writes the yank buffer back into the focused
|
|
120
|
+
pane.
|
|
84
121
|
|
|
85
122
|
## Commands (typed after `C-a :`)
|
|
86
123
|
|
data/lib/muxr/application.rb
CHANGED
|
@@ -302,6 +302,7 @@ module Muxr
|
|
|
302
302
|
def exit_selection(yank:)
|
|
303
303
|
target = focused_target
|
|
304
304
|
term = target&.terminal
|
|
305
|
+
yanked = false
|
|
305
306
|
if yank
|
|
306
307
|
# No anchor → no-op. User is still positioning; they can press v
|
|
307
308
|
# first, then yank. Esc/q is the way to exit from navigation.
|
|
@@ -311,10 +312,18 @@ module Muxr
|
|
|
311
312
|
@paste_buffer = text
|
|
312
313
|
spawn_pbcopy(text)
|
|
313
314
|
flash("yanked #{text.bytesize} bytes")
|
|
315
|
+
yanked = true
|
|
314
316
|
end
|
|
315
317
|
end
|
|
316
318
|
term&.clear_selection
|
|
317
|
-
|
|
319
|
+
if yanked
|
|
320
|
+
# vim-style: yanking drops you straight back to "normal" (idle),
|
|
321
|
+
# not back into scrollback navigation.
|
|
322
|
+
term&.scroll_to_bottom
|
|
323
|
+
@input.enter_idle_mode
|
|
324
|
+
else
|
|
325
|
+
@input.enter_scrollback_mode
|
|
326
|
+
end
|
|
318
327
|
@renderer.reset_frame!
|
|
319
328
|
invalidate
|
|
320
329
|
end
|
|
@@ -336,8 +345,18 @@ module Muxr
|
|
|
336
345
|
when :full_down then term.move_selection_cursor_by([rows - 1, 1].max, 0)
|
|
337
346
|
when :line_start then term.selection_cursor_to_line_start
|
|
338
347
|
when :line_end then term.selection_cursor_to_line_end
|
|
339
|
-
when :
|
|
340
|
-
when :
|
|
348
|
+
when :line_first_nonblank then term.selection_cursor_to_first_non_blank
|
|
349
|
+
when :top then term.selection_cursor_to_top
|
|
350
|
+
when :bottom then term.selection_cursor_to_bottom
|
|
351
|
+
when :screen_top then term.selection_cursor_to_viewport(:top)
|
|
352
|
+
when :screen_middle then term.selection_cursor_to_viewport(:middle)
|
|
353
|
+
when :screen_bottom then term.selection_cursor_to_viewport(:bottom)
|
|
354
|
+
when :word_forward then term.selection_cursor_word_forward(big: false)
|
|
355
|
+
when :word_forward_big then term.selection_cursor_word_forward(big: true)
|
|
356
|
+
when :word_end then term.selection_cursor_word_end(big: false)
|
|
357
|
+
when :word_end_big then term.selection_cursor_word_end(big: true)
|
|
358
|
+
when :word_backward then term.selection_cursor_word_backward(big: false)
|
|
359
|
+
when :word_backward_big then term.selection_cursor_word_backward(big: true)
|
|
341
360
|
end
|
|
342
361
|
invalidate
|
|
343
362
|
end
|
data/lib/muxr/input_handler.rb
CHANGED
|
@@ -48,8 +48,19 @@ module Muxr
|
|
|
48
48
|
"k" => :up,
|
|
49
49
|
"0" => :line_start,
|
|
50
50
|
"$" => :line_end,
|
|
51
|
+
"^" => :line_first_nonblank,
|
|
51
52
|
"g" => :top,
|
|
52
53
|
"G" => :bottom,
|
|
54
|
+
"H" => :screen_top,
|
|
55
|
+
"M" => :screen_middle,
|
|
56
|
+
"L" => :screen_bottom,
|
|
57
|
+
"w" => :word_forward,
|
|
58
|
+
"W" => :word_forward_big,
|
|
59
|
+
"e" => :word_end,
|
|
60
|
+
"E" => :word_end_big,
|
|
61
|
+
# `b` is vim word-back here; the tmux-style page-back alias lives on Ctrl-b.
|
|
62
|
+
"b" => :word_backward,
|
|
63
|
+
"B" => :word_backward_big,
|
|
53
64
|
"\x04" => :half_down, # Ctrl-d
|
|
54
65
|
"\x15" => :half_up, # Ctrl-u
|
|
55
66
|
"d" => :half_down,
|
|
@@ -57,7 +68,6 @@ module Muxr
|
|
|
57
68
|
"\x06" => :full_down, # Ctrl-f
|
|
58
69
|
"\x02" => :full_up, # Ctrl-b
|
|
59
70
|
"f" => :full_down,
|
|
60
|
-
"b" => :full_up,
|
|
61
71
|
" " => :full_down
|
|
62
72
|
}.freeze
|
|
63
73
|
|
data/lib/muxr/renderer.rb
CHANGED
|
@@ -264,8 +264,9 @@ module Muxr
|
|
|
264
264
|
" C-a Tab cycle layout (tall → grid → monocle)",
|
|
265
265
|
" C-a Enter promote focused pane to master",
|
|
266
266
|
" C-a ~ toggle drawer",
|
|
267
|
-
" C-a [ enter scrollback (j/k d/u f
|
|
268
|
-
"
|
|
267
|
+
" C-a [ enter scrollback (j/k d/u f g/G C-b/C-f; v→cursor, q quits)",
|
|
268
|
+
" cursor: v select, C-v block, y yank, q cancel",
|
|
269
|
+
" motions: h/j/k/l 0/^/$ w/e/b W/E/B H/M/L g/G",
|
|
269
270
|
" C-a ] paste internal copy buffer",
|
|
270
271
|
" C-a d detach (server keeps running)",
|
|
271
272
|
" C-a q kill session (asks y/n)",
|
|
@@ -439,6 +440,7 @@ module Muxr
|
|
|
439
440
|
parts = ["0"]
|
|
440
441
|
attrs = cell.attrs.to_i
|
|
441
442
|
parts << "1" if (attrs & Terminal::BOLD) != 0
|
|
443
|
+
parts << "2" if (attrs & Terminal::DIM) != 0
|
|
442
444
|
parts << "4" if (attrs & Terminal::UNDERLINE) != 0
|
|
443
445
|
parts << "7" if (attrs & Terminal::REVERSE) != 0
|
|
444
446
|
append_color(parts, cell.fg, true)
|
data/lib/muxr/terminal.rb
CHANGED
|
@@ -8,6 +8,7 @@ module Muxr
|
|
|
8
8
|
BOLD = 1
|
|
9
9
|
UNDERLINE = 2
|
|
10
10
|
REVERSE = 4
|
|
11
|
+
DIM = 8
|
|
11
12
|
|
|
12
13
|
SCROLLBACK_MAX = 5000
|
|
13
14
|
|
|
@@ -183,6 +184,96 @@ module Muxr
|
|
|
183
184
|
selection_cursor_to(timeline_size - 1, @cols - 1)
|
|
184
185
|
end
|
|
185
186
|
|
|
187
|
+
def selection_cursor_to_first_non_blank
|
|
188
|
+
return unless @selection_cursor
|
|
189
|
+
tr = @selection_cursor[0]
|
|
190
|
+
selection_cursor_to(tr, first_non_blank_col(tr))
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Jump to top/middle/bottom of the visible viewport (vim H/M/L), landing
|
|
194
|
+
# on the first non-blank column of the destination line.
|
|
195
|
+
def selection_cursor_to_viewport(where)
|
|
196
|
+
return unless @selection_cursor
|
|
197
|
+
vr = case where
|
|
198
|
+
when :top then 0
|
|
199
|
+
when :middle then @rows / 2
|
|
200
|
+
when :bottom then @rows - 1
|
|
201
|
+
end
|
|
202
|
+
return if vr.nil?
|
|
203
|
+
tr = timeline_row_for_visible(vr).clamp(0, timeline_size - 1)
|
|
204
|
+
selection_cursor_to(tr, first_non_blank_col(tr))
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def selection_cursor_word_forward(big: false)
|
|
208
|
+
return unless @selection_cursor
|
|
209
|
+
tr, tc = @selection_cursor
|
|
210
|
+
prev_cls = char_class_at(tr, tc, big: big)
|
|
211
|
+
loop do
|
|
212
|
+
nxt = step_forward(tr, tc)
|
|
213
|
+
break unless nxt
|
|
214
|
+
ntr, ntc = nxt
|
|
215
|
+
cur_cls = char_class_at(ntr, ntc, big: big)
|
|
216
|
+
# Row boundaries act as whitespace breaks even when the row is fully
|
|
217
|
+
# packed (no trailing pad) — visually the user sees a new line.
|
|
218
|
+
effective_prev = (ntr != tr) ? :space : prev_cls
|
|
219
|
+
if effective_prev != cur_cls && cur_cls != :space
|
|
220
|
+
selection_cursor_to(ntr, ntc)
|
|
221
|
+
return
|
|
222
|
+
end
|
|
223
|
+
tr, tc = ntr, ntc
|
|
224
|
+
prev_cls = cur_cls
|
|
225
|
+
end
|
|
226
|
+
selection_cursor_to(timeline_size - 1, @cols - 1)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def selection_cursor_word_end(big: false)
|
|
230
|
+
return unless @selection_cursor
|
|
231
|
+
tr, tc = @selection_cursor
|
|
232
|
+
pos = step_forward(tr, tc)
|
|
233
|
+
return unless pos
|
|
234
|
+
tr, tc = pos
|
|
235
|
+
while char_class_at(tr, tc, big: big) == :space
|
|
236
|
+
pos = step_forward(tr, tc)
|
|
237
|
+
break unless pos
|
|
238
|
+
tr, tc = pos
|
|
239
|
+
end
|
|
240
|
+
return if char_class_at(tr, tc, big: big) == :space
|
|
241
|
+
cls = char_class_at(tr, tc, big: big)
|
|
242
|
+
loop do
|
|
243
|
+
pos = step_forward(tr, tc)
|
|
244
|
+
if pos.nil? || pos[0] != tr || char_class_at(pos[0], pos[1], big: big) != cls
|
|
245
|
+
selection_cursor_to(tr, tc)
|
|
246
|
+
return
|
|
247
|
+
end
|
|
248
|
+
tr, tc = pos
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def selection_cursor_word_backward(big: false)
|
|
253
|
+
return unless @selection_cursor
|
|
254
|
+
tr, tc = @selection_cursor
|
|
255
|
+
pos = step_backward(tr, tc)
|
|
256
|
+
return unless pos
|
|
257
|
+
tr, tc = pos
|
|
258
|
+
while char_class_at(tr, tc, big: big) == :space
|
|
259
|
+
pos = step_backward(tr, tc)
|
|
260
|
+
unless pos
|
|
261
|
+
selection_cursor_to(tr, tc)
|
|
262
|
+
return
|
|
263
|
+
end
|
|
264
|
+
tr, tc = pos
|
|
265
|
+
end
|
|
266
|
+
cls = char_class_at(tr, tc, big: big)
|
|
267
|
+
loop do
|
|
268
|
+
pos = step_backward(tr, tc)
|
|
269
|
+
if pos.nil? || pos[0] != tr || char_class_at(pos[0], pos[1], big: big) != cls
|
|
270
|
+
selection_cursor_to(tr, tc)
|
|
271
|
+
return
|
|
272
|
+
end
|
|
273
|
+
tr, tc = pos
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
186
277
|
def clear_selection
|
|
187
278
|
return unless @selection_anchor
|
|
188
279
|
@selection_anchor = nil
|
|
@@ -594,6 +685,44 @@ module Muxr
|
|
|
594
685
|
@scrollback.size - @view_offset + r
|
|
595
686
|
end
|
|
596
687
|
|
|
688
|
+
def first_non_blank_col(tr)
|
|
689
|
+
row = timeline_row(tr)
|
|
690
|
+
return 0 unless row
|
|
691
|
+
@cols.times do |c|
|
|
692
|
+
ch = row[c]&.char
|
|
693
|
+
return c if ch && ch != " " && ch != "\t"
|
|
694
|
+
end
|
|
695
|
+
0
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
def char_class_at(tr, tc, big:)
|
|
699
|
+
row = timeline_row(tr)
|
|
700
|
+
classify_char(row && row[tc] && row[tc].char, big: big)
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
# vim "word" = run of \w (alnum + _); "WORD" = any run of non-whitespace.
|
|
704
|
+
def classify_char(ch, big:)
|
|
705
|
+
return :space if ch.nil? || ch == " " || ch == "\t" || ch == ""
|
|
706
|
+
return :word if big
|
|
707
|
+
ch.match?(/\A\w\z/) ? :word : :punct
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
def step_forward(tr, tc)
|
|
711
|
+
if tc + 1 < @cols
|
|
712
|
+
[tr, tc + 1]
|
|
713
|
+
elsif tr + 1 < timeline_size
|
|
714
|
+
[tr + 1, 0]
|
|
715
|
+
end
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
def step_backward(tr, tc)
|
|
719
|
+
if tc > 0
|
|
720
|
+
[tr, tc - 1]
|
|
721
|
+
elsif tr > 0
|
|
722
|
+
[tr - 1, @cols - 1]
|
|
723
|
+
end
|
|
724
|
+
end
|
|
725
|
+
|
|
597
726
|
def ordered_selection
|
|
598
727
|
a = @selection_anchor
|
|
599
728
|
b = @selection_cursor
|
|
@@ -719,9 +848,10 @@ module Muxr
|
|
|
719
848
|
@bg = nil
|
|
720
849
|
@attrs = 0
|
|
721
850
|
when 1 then @attrs |= BOLD
|
|
851
|
+
when 2 then @attrs |= DIM
|
|
722
852
|
when 4 then @attrs |= UNDERLINE
|
|
723
853
|
when 7 then @attrs |= REVERSE
|
|
724
|
-
when 22 then @attrs &= ~BOLD
|
|
854
|
+
when 22 then @attrs &= ~(BOLD | DIM)
|
|
725
855
|
when 24 then @attrs &= ~UNDERLINE
|
|
726
856
|
when 27 then @attrs &= ~REVERSE
|
|
727
857
|
when 30..37 then @fg = p - 30
|
data/lib/muxr/version.rb
CHANGED