muxr 0.1.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e431ecab7ea6e49cf31e2865c3c870603847e422c9c1e5dfc8b86ff413b95f5
4
- data.tar.gz: e6db50aba85439befa78b6c89ec5dcd09f6d3cfa66195e0d552f403c229b7932
3
+ metadata.gz: ab53a383e7781f83ff106e503047cf5952b12193d0d65e2b29631f2beda6bcf5
4
+ data.tar.gz: 0fcef17549ae1745d76fe530a97f12740c9556704bbe724f1b4690932c7cabc5
5
5
  SHA512:
6
- metadata.gz: 2f0513da3cfba34c6efbc73c398934ada15ccb00b29958d4b5bf70b397cb7110819da8915acbbd13db4a8dd9d922551e3bf70d09c2dbf6247e2ebea598d2f9b5
7
- data.tar.gz: 9f0dcefe6bd25d5173044d88b81fc4efac0872b5961f010ed33e370335f341f2ee2ec98b5d0ed1ee50036a3675ba96ad3b33b50d95397ef32c6726eaabe0a29a
6
+ metadata.gz: 4440cb89e4295407dce42ca26c474c64441ff556cb285769b085e0474448132085d9bb6ad9af91699e6349a915c90fe6ebd33825d7ee68f315cafc26ccac5244
7
+ data.tar.gz: e17c2f092dcd76ca617bfbfd6acdf1e5475ea9fd7afe0a7458539af95eaa32b0282d499a094f19a6887b6fd228f03d470d5a8c6b9b167b9838e6989edd20ee74
data/CHANGELOG.md CHANGED
@@ -6,6 +6,31 @@ 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
+
25
+ ## [0.1.1] - 2026-05-11
26
+
27
+ ### Fixed
28
+ - `muxr --list` now reports sessions whose server is actually running
29
+ (live sockets in `~/.muxr/sockets/`) instead of `~/.muxr/sessions/*.json`,
30
+ which only exist after an explicit `:save` and so missed every live
31
+ session. The saved-snapshot enumeration is still available internally
32
+ via `Muxr::Session.list`.
33
+
9
34
  ## [0.1.0] - 2026-05-11
10
35
 
11
36
  Initial release.
@@ -42,5 +67,7 @@ Initial release.
42
67
  boundaries.
43
68
  - Renderer that composes one frame and diff-emits ANSI to STDOUT.
44
69
 
45
- [Unreleased]: https://github.com/roelbondoc/muxr/compare/v0.1.0...HEAD
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
72
+ [0.1.1]: https://github.com/roelbondoc/muxr/releases/tag/v0.1.1
46
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
+ ![drawer overlay](docs/screenshots/04-drawer.png)
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` / `b` / Space (or `C-f`/`C-b`) | full page |
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
- (`h j k l`, `0`/`$`, `g`/`G`, `C-d`/`C-u`, `C-f`/`C-b`, Space). Press `v`
79
- again to anchor a character selection or `C-v` to anchor a block
80
- (rectangular) selection — toggling between the two preserves the anchor.
81
- `y` or Enter yanks the selection into an internal buffer and pipes it to
82
- `pbcopy` in the background (silent no-op when `pbcopy` is unavailable).
83
- `C-a ]` writes the yank buffer back into the focused pane.
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/bin/muxr CHANGED
@@ -13,7 +13,7 @@ if ARGV.include?("-v") || ARGV.include?("--version")
13
13
  end
14
14
 
15
15
  if ARGV.include?("-l") || ARGV.include?("--list")
16
- names = Muxr::Session.list
16
+ names = Muxr::Application.list_active
17
17
  puts names.join("\n") unless names.empty?
18
18
  exit 0
19
19
  end
@@ -26,7 +26,7 @@ if ARGV.include?("-h") || ARGV.include?("--help")
26
26
  muxr attach the default session (auto-spawn if needed)
27
27
  muxr <name> attach (or start) the named session
28
28
  muxr -s <name> same as above
29
- muxr --list list saved session files and exit
29
+ muxr --list list running sessions and exit
30
30
  muxr --version print version and exit
31
31
  muxr --help this help
32
32
 
@@ -26,6 +26,27 @@ module Muxr
26
26
  File.join(SOCKETS_DIR, "#{name}.sock")
27
27
  end
28
28
 
29
+ # Names of sessions whose server socket is currently accepting connections.
30
+ # Stale sockets (file exists, no listener) are skipped but left in place;
31
+ # cleanup happens on the next attach attempt.
32
+ def self.list_active
33
+ return [] unless File.directory?(SOCKETS_DIR)
34
+ Dir.children(SOCKETS_DIR).filter_map do |entry|
35
+ next unless entry.end_with?(".sock")
36
+ path = File.join(SOCKETS_DIR, entry)
37
+ next unless alive_socket?(path)
38
+ File.basename(entry, ".sock")
39
+ end.sort
40
+ end
41
+
42
+ def self.alive_socket?(path)
43
+ return false unless File.exist?(path)
44
+ UNIXSocket.new(path).close
45
+ true
46
+ rescue SystemCallError
47
+ false
48
+ end
49
+
29
50
  def initialize(argv = [])
30
51
  @argv = argv
31
52
  @session_name = parse_session_name(argv)
@@ -281,6 +302,7 @@ module Muxr
281
302
  def exit_selection(yank:)
282
303
  target = focused_target
283
304
  term = target&.terminal
305
+ yanked = false
284
306
  if yank
285
307
  # No anchor → no-op. User is still positioning; they can press v
286
308
  # first, then yank. Esc/q is the way to exit from navigation.
@@ -290,10 +312,18 @@ module Muxr
290
312
  @paste_buffer = text
291
313
  spawn_pbcopy(text)
292
314
  flash("yanked #{text.bytesize} bytes")
315
+ yanked = true
293
316
  end
294
317
  end
295
318
  term&.clear_selection
296
- @input.enter_scrollback_mode
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
297
327
  @renderer.reset_frame!
298
328
  invalidate
299
329
  end
@@ -315,8 +345,18 @@ module Muxr
315
345
  when :full_down then term.move_selection_cursor_by([rows - 1, 1].max, 0)
316
346
  when :line_start then term.selection_cursor_to_line_start
317
347
  when :line_end then term.selection_cursor_to_line_end
318
- when :top then term.selection_cursor_to_top
319
- when :bottom then term.selection_cursor_to_bottom
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)
320
360
  end
321
361
  invalidate
322
362
  end
@@ -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/b g/G; v→cursor, q quits)",
268
- " in cursor mode: v char-select, C-v block-select, y yank",
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
@@ -1,3 +1,3 @@
1
1
  module Muxr
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.2"
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.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roel Bondoc