muxr 0.1.5 → 0.1.6
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 +46 -0
- data/README.md +210 -65
- data/lib/muxr/application.rb +126 -0
- data/lib/muxr/foreground_command.rb +86 -0
- data/lib/muxr/input_handler.rb +131 -27
- data/lib/muxr/layout_manager.rb +59 -0
- data/lib/muxr/pane.rb +10 -0
- data/lib/muxr/renderer.rb +136 -35
- data/lib/muxr/terminal.rb +44 -2
- data/lib/muxr/version.rb +1 -1
- data/lib/muxr.rb +1 -0
- metadata +2 -1
data/lib/muxr/renderer.rb
CHANGED
|
@@ -4,13 +4,30 @@ module Muxr
|
|
|
4
4
|
# new frame against the previous one and only repositions/redraws cells
|
|
5
5
|
# whose contents changed, keeping output volume low between ticks.
|
|
6
6
|
class Renderer
|
|
7
|
-
BORDER_FOCUSED = [:c256, 11].freeze # yellow
|
|
7
|
+
BORDER_FOCUSED = [:c256, 11].freeze # yellow (fallback when no mode color)
|
|
8
8
|
BORDER_UNFOCUSED = [:c256, 8].freeze # grey
|
|
9
9
|
BORDER_DRAWER_FOCUS = [:c256, 13].freeze # magenta
|
|
10
10
|
BORDER_DRAWER_IDLE = [:c256, 5].freeze # dark magenta
|
|
11
11
|
STATUS_BG = [:c256, 236].freeze
|
|
12
12
|
STATUS_FG = [:c256, 252].freeze
|
|
13
13
|
|
|
14
|
+
# Vim-style mode palette. Used in two places: the focused pane border
|
|
15
|
+
# (so the user can see at a glance what mode they're in) and the
|
|
16
|
+
# [MODE] chip in the status bar (same color, smaller real estate).
|
|
17
|
+
# :prefix maps to the same green as :passthrough because :prefix is a
|
|
18
|
+
# transient sub-state under passthrough — sharing the color avoids a
|
|
19
|
+
# one-frame border flicker when pressing Ctrl-a.
|
|
20
|
+
MODE_COLOR = {
|
|
21
|
+
normal: [:c256, 51].freeze, # cyan
|
|
22
|
+
passthrough: [:c256, 42].freeze, # green
|
|
23
|
+
prefix: [:c256, 42].freeze, # green (passthrough sub-state)
|
|
24
|
+
command: [:c256, 226].freeze, # yellow
|
|
25
|
+
scrollback: [:c256, 214].freeze, # orange
|
|
26
|
+
selection: [:c256, 201].freeze, # magenta
|
|
27
|
+
confirm_quit: [:c256, 196].freeze, # red
|
|
28
|
+
help: [:c256, 39].freeze # blue
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
14
31
|
HORIZONTAL = "─".freeze
|
|
15
32
|
VERTICAL = "│".freeze
|
|
16
33
|
TL = "┌".freeze
|
|
@@ -18,9 +35,9 @@ module Muxr
|
|
|
18
35
|
BL = "└".freeze
|
|
19
36
|
BR = "┘".freeze
|
|
20
37
|
|
|
21
|
-
Cell = Struct.new(:char, :fg, :bg, :attrs) do
|
|
38
|
+
Cell = Struct.new(:char, :fg, :bg, :attrs, :hyperlink) do
|
|
22
39
|
def ==(other)
|
|
23
|
-
other.is_a?(Cell) && char == other.char && fg == other.fg && bg == other.bg && attrs == other.attrs
|
|
40
|
+
other.is_a?(Cell) && char == other.char && fg == other.fg && bg == other.bg && attrs == other.attrs && hyperlink == other.hyperlink
|
|
24
41
|
end
|
|
25
42
|
end
|
|
26
43
|
|
|
@@ -32,7 +49,10 @@ module Muxr
|
|
|
32
49
|
end
|
|
33
50
|
|
|
34
51
|
def enter_alt_screen
|
|
35
|
-
|
|
52
|
+
# Close any stale OSC 8 hyperlink the outer terminal might be carrying
|
|
53
|
+
# from before we attached, so the first frame's run-tracker matches
|
|
54
|
+
# reality.
|
|
55
|
+
@out.write("\e[?1049h\e[?25l\e[2J\e[H\e[0m\e]8;;\e\\")
|
|
36
56
|
@out.flush
|
|
37
57
|
@prev = nil
|
|
38
58
|
end
|
|
@@ -46,15 +66,15 @@ module Muxr
|
|
|
46
66
|
@prev = nil
|
|
47
67
|
end
|
|
48
68
|
|
|
49
|
-
def render(session, input_state: :
|
|
69
|
+
def render(session, input_state: :normal, command_buffer: "", message: nil, help: false)
|
|
50
70
|
w = session.width
|
|
51
71
|
h = session.height
|
|
52
72
|
return if w < 4 || h < 3
|
|
53
73
|
|
|
54
|
-
frame = Array.new(h) { Array.new(w) { Cell.new(" ", nil, nil, 0) } }
|
|
74
|
+
frame = Array.new(h) { Array.new(w) { Cell.new(" ", nil, nil, 0, nil) } }
|
|
55
75
|
|
|
56
|
-
compose_panes(frame, session)
|
|
57
|
-
compose_drawer(frame, session) if session.drawer&.visible?
|
|
76
|
+
compose_panes(frame, session, input_state: input_state)
|
|
77
|
+
compose_drawer(frame, session, input_state: input_state) if session.drawer&.visible?
|
|
58
78
|
compose_status_bar(frame, session, input_state: input_state, command_buffer: command_buffer, message: message)
|
|
59
79
|
compose_help(frame, session) if help
|
|
60
80
|
|
|
@@ -63,7 +83,7 @@ module Muxr
|
|
|
63
83
|
|
|
64
84
|
private
|
|
65
85
|
|
|
66
|
-
def compose_panes(frame, session)
|
|
86
|
+
def compose_panes(frame, session, input_state: :normal)
|
|
67
87
|
win = session.window
|
|
68
88
|
content_area = LayoutManager::Rect.new(0, 0, session.width, session.height - 1)
|
|
69
89
|
rects = LayoutManager.compute(
|
|
@@ -99,20 +119,32 @@ module Muxr
|
|
|
99
119
|
title += " #{pane.id}" if pane.respond_to?(:id) && pane.id.is_a?(String)
|
|
100
120
|
title += " [P]" if pane.respond_to?(:private?) && pane.private?
|
|
101
121
|
title += " ★" if i == win.master_index
|
|
102
|
-
|
|
122
|
+
# Foreground command (e.g. "npm test", "vim"). Set by the poller
|
|
123
|
+
# thread; nil when the shell itself is foreground. Truncate so a
|
|
124
|
+
# long invocation doesn't push the title past what draw_box will
|
|
125
|
+
# render — draw_box silently drops titles that don't fit.
|
|
126
|
+
if pane.respond_to?(:foreground_command) && pane.foreground_command
|
|
127
|
+
cmd = pane.foreground_command.to_s[0, 16]
|
|
128
|
+
title += " · #{cmd}"
|
|
129
|
+
end
|
|
103
130
|
if pane.terminal.scrolled_back?
|
|
104
131
|
title += " [scrollback #{pane.terminal.view_offset}/#{pane.terminal.scrollback_size}]"
|
|
105
132
|
end
|
|
106
133
|
draw_box(frame, rect,
|
|
107
|
-
border: focused ?
|
|
134
|
+
border: focused ? mode_color(input_state) : BORDER_UNFOCUSED,
|
|
108
135
|
bold_border: focused,
|
|
109
136
|
title: title,
|
|
110
137
|
title_focused: focused)
|
|
138
|
+
# Mode chip lives in the top-right corner of the focused container
|
|
139
|
+
# (this pane, or the drawer — see compose_drawer). Showing it on
|
|
140
|
+
# the same edge as the title but on the opposite side keeps both
|
|
141
|
+
# readable without one crowding the other.
|
|
142
|
+
draw_mode_chip(frame, rect, input_state, title) if focused
|
|
111
143
|
copy_terminal(frame, pane, rect.x + 1, rect.y + 1)
|
|
112
144
|
end
|
|
113
145
|
end
|
|
114
146
|
|
|
115
|
-
def compose_drawer(frame, session)
|
|
147
|
+
def compose_drawer(frame, session, input_state: :normal)
|
|
116
148
|
drawer = session.drawer
|
|
117
149
|
return unless drawer&.pane
|
|
118
150
|
|
|
@@ -150,9 +182,47 @@ module Muxr
|
|
|
150
182
|
bold_border: true,
|
|
151
183
|
title: title,
|
|
152
184
|
title_focused: focused)
|
|
185
|
+
draw_mode_chip(frame, rect, input_state, title) if focused
|
|
153
186
|
copy_terminal(frame, drawer.pane, rect.x + 1, rect.y + 1)
|
|
154
187
|
end
|
|
155
188
|
|
|
189
|
+
def mode_color(input_state)
|
|
190
|
+
MODE_COLOR[input_state] || BORDER_FOCUSED
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Paint the " [MODE] " chip on the top border, hugging the right corner.
|
|
194
|
+
# Skipped when there isn't at least one column of breathing room between
|
|
195
|
+
# the title (anchored at the top-left) and the chip — otherwise long
|
|
196
|
+
# titles + a wide chip would overdraw each other and produce garbage.
|
|
197
|
+
def draw_mode_chip(frame, rect, input_state, title)
|
|
198
|
+
chip = " [#{mode_label(input_state)}] "
|
|
199
|
+
chip_start = rect.x + rect.w - 1 - chip.length
|
|
200
|
+
title_text = " #{title} "
|
|
201
|
+
title_end = rect.x + 2 + title_text.length - 1
|
|
202
|
+
return if chip_start <= title_end + 1
|
|
203
|
+
chip_color = mode_color(input_state)
|
|
204
|
+
chip.each_char.with_index do |ch, j|
|
|
205
|
+
set_cell(frame, rect.y, chip_start + j, ch, fg: chip_color, attrs: Terminal::BOLD)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Two-letter-ish mode label shown in the leftmost slot of the status bar.
|
|
210
|
+
# Lets the user see at a glance whether single-key bindings are active
|
|
211
|
+
# (NORMAL) or every key passes through to the focused pane (PASS).
|
|
212
|
+
def mode_label(input_state)
|
|
213
|
+
case input_state
|
|
214
|
+
when :normal then "NORMAL"
|
|
215
|
+
when :passthrough then "PASS"
|
|
216
|
+
when :prefix then "^A"
|
|
217
|
+
when :command then "CMD"
|
|
218
|
+
when :scrollback then "SCROLL"
|
|
219
|
+
when :selection then "SEL"
|
|
220
|
+
when :confirm_quit then "QUIT?"
|
|
221
|
+
when :help then "HELP"
|
|
222
|
+
else "?"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
156
226
|
def compose_status_bar(frame, session, input_state:, command_buffer:, message:)
|
|
157
227
|
y = session.height - 1
|
|
158
228
|
w = session.width
|
|
@@ -163,7 +233,8 @@ module Muxr
|
|
|
163
233
|
else "hidden"
|
|
164
234
|
end
|
|
165
235
|
|
|
166
|
-
left = " [#{
|
|
236
|
+
left = " [#{mode_label(input_state)}]"
|
|
237
|
+
left << " [#{session.name}]"
|
|
167
238
|
left << " panes:#{win.panes.length}"
|
|
168
239
|
left << " layout:#{win.layout}"
|
|
169
240
|
focused_label =
|
|
@@ -190,6 +261,21 @@ module Muxr
|
|
|
190
261
|
c.attrs = 0
|
|
191
262
|
end
|
|
192
263
|
|
|
264
|
+
# Recolor the leading "[MODE]" chip in the mode's accent color. The
|
|
265
|
+
# chip lives at offset 1 (after one leading space) and runs for
|
|
266
|
+
# bracket+label+bracket characters. Full-row overlays below will
|
|
267
|
+
# overwrite this when active (command/scrollback/selection) — that's
|
|
268
|
+
# fine, those modes already convey themselves loudly.
|
|
269
|
+
chip = "[#{mode_label(input_state)}]"
|
|
270
|
+
chip_color = mode_color(input_state)
|
|
271
|
+
chip_start = 1
|
|
272
|
+
chip_end = [chip_start + chip.length, w].min
|
|
273
|
+
(chip_start...chip_end).each do |x|
|
|
274
|
+
c = frame[y][x]
|
|
275
|
+
c.fg = chip_color
|
|
276
|
+
c.attrs |= Terminal::BOLD
|
|
277
|
+
end
|
|
278
|
+
|
|
193
279
|
if input_state == :command
|
|
194
280
|
overlay = ":#{command_buffer}"
|
|
195
281
|
overlay = overlay[0, w]
|
|
@@ -231,7 +317,7 @@ module Muxr
|
|
|
231
317
|
mode = focused.terminal.selection_mode == :block ? "BLOCK" : "CHAR"
|
|
232
318
|
end
|
|
233
319
|
label = mode ? "SELECTION/#{mode}" : "SELECTION (cursor)"
|
|
234
|
-
overlay = " #{label} h/j/k/l move v char C-v block y/Enter yank q cancel "
|
|
320
|
+
overlay = " #{label} h/j/k/l move v/space char C-v block y/Enter yank q cancel "
|
|
235
321
|
overlay = overlay[0, w]
|
|
236
322
|
overlay.each_char.with_index do |ch, x|
|
|
237
323
|
c = frame[y][x]
|
|
@@ -265,28 +351,33 @@ module Muxr
|
|
|
265
351
|
HELP_LINES = [
|
|
266
352
|
"muxr — keybindings",
|
|
267
353
|
"",
|
|
268
|
-
"
|
|
269
|
-
"
|
|
270
|
-
"
|
|
271
|
-
"
|
|
272
|
-
"
|
|
273
|
-
"
|
|
274
|
-
"
|
|
275
|
-
"
|
|
276
|
-
" C
|
|
277
|
-
"
|
|
278
|
-
"
|
|
279
|
-
"
|
|
280
|
-
"
|
|
281
|
-
" C-a
|
|
282
|
-
" C-a
|
|
283
|
-
" C-a
|
|
284
|
-
" C-a
|
|
285
|
-
" C-a
|
|
286
|
-
" C-a C-a
|
|
354
|
+
"NORMAL mode (default; no prefix)",
|
|
355
|
+
" h / j / k / l focus pane left / down / up / right",
|
|
356
|
+
" i drop into passthrough mode",
|
|
357
|
+
" c / K new / close pane",
|
|
358
|
+
" t / g / m layout: tall / grid / monocle",
|
|
359
|
+
" Tab / Enter cycle layout / promote to master",
|
|
360
|
+
" a / 1..9 last pane / jump by number",
|
|
361
|
+
" s enter scrollback",
|
|
362
|
+
" ~ / C / P drawer / Claude drawer / toggle private",
|
|
363
|
+
" : / ? command prompt / toggle this help",
|
|
364
|
+
" ] / d / q paste buffer / detach / kill session",
|
|
365
|
+
"",
|
|
366
|
+
"PASSTHROUGH mode (keys reach the focused pane; prefix is Ctrl-a)",
|
|
367
|
+
" C-a Esc return to normal mode",
|
|
368
|
+
" C-a c K t g m same as normal-mode bindings",
|
|
369
|
+
" C-a Tab Enter cycle layout / promote master",
|
|
370
|
+
" C-a n / p / a next / prev / last pane",
|
|
371
|
+
" C-a [ ] scrollback / paste buffer",
|
|
372
|
+
" C-a C-a send literal Ctrl-a to focused pane",
|
|
287
373
|
"",
|
|
288
|
-
"
|
|
289
|
-
"
|
|
374
|
+
"SCROLLBACK mode (exits to the mode you came from)",
|
|
375
|
+
" j/k d/u f/b g/G scroll C-b/C-f page v→cursor",
|
|
376
|
+
" cursor: h/j/k/l 0/^/$ w/e/b W/E/B H/M/L g/G",
|
|
377
|
+
" v select, C-v block, y/Enter yank, q/Esc cancel",
|
|
378
|
+
"",
|
|
379
|
+
"Commands: layout {tall|grid|monocle}, drawer {toggle|show|hide|reset},",
|
|
380
|
+
" claude, save, restore, sessions, quit, new, close, next, prev",
|
|
290
381
|
"",
|
|
291
382
|
"press any key to dismiss"
|
|
292
383
|
].freeze
|
|
@@ -367,6 +458,7 @@ module Muxr
|
|
|
367
458
|
dst.bg = src.bg
|
|
368
459
|
dst.attrs = src.attrs
|
|
369
460
|
dst.attrs |= Terminal::REVERSE if selection && term.selected_at_visible?(r, c)
|
|
461
|
+
dst.hyperlink = src.hyperlink
|
|
370
462
|
end
|
|
371
463
|
end
|
|
372
464
|
end
|
|
@@ -393,6 +485,9 @@ module Muxr
|
|
|
393
485
|
cur_fg = :unset
|
|
394
486
|
cur_bg = :unset
|
|
395
487
|
cur_attrs = :unset
|
|
488
|
+
# We close any open hyperlink at end-of-frame, so the outer terminal
|
|
489
|
+
# always starts a new frame in the "no hyperlink" state.
|
|
490
|
+
cur_hyperlink = nil
|
|
396
491
|
last_y = nil
|
|
397
492
|
last_x = nil
|
|
398
493
|
|
|
@@ -410,11 +505,17 @@ module Muxr
|
|
|
410
505
|
cur_bg = cell.bg
|
|
411
506
|
cur_attrs = cell.attrs
|
|
412
507
|
end
|
|
508
|
+
if cell.hyperlink != cur_hyperlink
|
|
509
|
+
out << "\e]8;;\e\\" if cur_hyperlink
|
|
510
|
+
out << "\e]#{cell.hyperlink}\e\\" if cell.hyperlink
|
|
511
|
+
cur_hyperlink = cell.hyperlink
|
|
512
|
+
end
|
|
413
513
|
out << cell.char
|
|
414
514
|
last_y = y
|
|
415
515
|
last_x = x + cell.char.length
|
|
416
516
|
end
|
|
417
517
|
end
|
|
518
|
+
out << "\e]8;;\e\\" if cur_hyperlink
|
|
418
519
|
out << "\e[0m"
|
|
419
520
|
out << cursor_position(session, input_state: input_state, command_buffer: command_buffer)
|
|
420
521
|
out << "\e[?2026l"
|
data/lib/muxr/terminal.rb
CHANGED
|
@@ -12,6 +12,11 @@ module Muxr
|
|
|
12
12
|
|
|
13
13
|
SCROLLBACK_MAX = 5000
|
|
14
14
|
|
|
15
|
+
# Cap on the OSC payload we buffer before parsing. URLs in OSC 8 can be
|
|
16
|
+
# long but rarely exceed a few hundred bytes; 4 KiB lets the parser stay
|
|
17
|
+
# tolerant of weird inputs without giving an attacker an unbounded sink.
|
|
18
|
+
OSC_MAX_LEN = 4096
|
|
19
|
+
|
|
15
20
|
# Inner programs (fzf ≥ 0.41, neovim, helix, …) bracket coherent screen
|
|
16
21
|
# updates with `\e[?2026h … \e[?2026l` (DECSET 2026 — "Synchronized
|
|
17
22
|
# Output"). When we see the open, we know more bytes are coming that
|
|
@@ -20,12 +25,13 @@ module Muxr
|
|
|
20
25
|
# program (which left ?2026h open) cannot wedge the pane indefinitely.
|
|
21
26
|
SYNC_TIMEOUT = 0.2
|
|
22
27
|
|
|
23
|
-
Cell = Struct.new(:char, :fg, :bg, :attrs) do
|
|
28
|
+
Cell = Struct.new(:char, :fg, :bg, :attrs, :hyperlink) do
|
|
24
29
|
def reset!
|
|
25
30
|
self.char = " "
|
|
26
31
|
self.fg = nil
|
|
27
32
|
self.bg = nil
|
|
28
33
|
self.attrs = 0
|
|
34
|
+
self.hyperlink = nil
|
|
29
35
|
end
|
|
30
36
|
|
|
31
37
|
def copy_from(other)
|
|
@@ -33,6 +39,7 @@ module Muxr
|
|
|
33
39
|
self.fg = other.fg
|
|
34
40
|
self.bg = other.bg
|
|
35
41
|
self.attrs = other.attrs
|
|
42
|
+
self.hyperlink = other.hyperlink
|
|
36
43
|
end
|
|
37
44
|
end
|
|
38
45
|
|
|
@@ -53,7 +60,14 @@ module Muxr
|
|
|
53
60
|
@scroll_bottom = rows - 1
|
|
54
61
|
@parser_state = :ground
|
|
55
62
|
@parser_params = +""
|
|
63
|
+
@parser_osc = +""
|
|
56
64
|
@feed_remainder = +"".b
|
|
65
|
+
# Currently-active OSC 8 hyperlink body (the "8;params;URI" payload that
|
|
66
|
+
# we'll wrap back around runs of cells when rendering), or nil when no
|
|
67
|
+
# hyperlink is open. Interned via @hyperlink_intern so repeated identical
|
|
68
|
+
# links share one frozen string for fast equality and small memory.
|
|
69
|
+
@current_hyperlink = nil
|
|
70
|
+
@hyperlink_intern = {}
|
|
57
71
|
@dirty = true
|
|
58
72
|
@scrollback = []
|
|
59
73
|
@view_offset = 0
|
|
@@ -448,7 +462,25 @@ module Muxr
|
|
|
448
462
|
private
|
|
449
463
|
|
|
450
464
|
def blank_cell
|
|
451
|
-
Cell.new(" ", nil, nil, 0)
|
|
465
|
+
Cell.new(" ", nil, nil, 0, nil)
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# Parse the just-completed OSC payload. We only care about OSC 8
|
|
469
|
+
# (hyperlinks): `8;params;URI`. An empty URI closes the active link.
|
|
470
|
+
# Anything else (window-title OSC 0/1/2, palette OSC 4, …) is silently
|
|
471
|
+
# consumed — the emulator doesn't model it.
|
|
472
|
+
def finalize_osc
|
|
473
|
+
payload = @parser_osc
|
|
474
|
+
@parser_osc = +""
|
|
475
|
+
return if payload.empty?
|
|
476
|
+
return unless payload.start_with?("8;")
|
|
477
|
+
parts = payload.split(";", 3)
|
|
478
|
+
uri = parts[2]
|
|
479
|
+
if uri.nil? || uri.empty?
|
|
480
|
+
@current_hyperlink = nil
|
|
481
|
+
else
|
|
482
|
+
@current_hyperlink = (@hyperlink_intern[payload] ||= payload.dup.freeze)
|
|
483
|
+
end
|
|
452
484
|
end
|
|
453
485
|
|
|
454
486
|
def process_char(ch)
|
|
@@ -462,11 +494,18 @@ module Muxr
|
|
|
462
494
|
csi_char(ch, b)
|
|
463
495
|
when :osc
|
|
464
496
|
if b == 0x07 || b == 0x9c
|
|
497
|
+
finalize_osc
|
|
465
498
|
@parser_state = :ground
|
|
466
499
|
elsif b == 0x1b
|
|
467
500
|
@parser_state = :osc_esc
|
|
501
|
+
elsif @parser_osc.bytesize < OSC_MAX_LEN
|
|
502
|
+
@parser_osc << ch
|
|
468
503
|
end
|
|
469
504
|
when :osc_esc
|
|
505
|
+
# ST is `ESC \`. Anything else is malformed but we still flush — most
|
|
506
|
+
# terminals are lenient here, and being strict would swallow the
|
|
507
|
+
# payload on slightly buggy emitters.
|
|
508
|
+
finalize_osc
|
|
470
509
|
@parser_state = :ground
|
|
471
510
|
when :charset
|
|
472
511
|
@parser_state = :ground
|
|
@@ -505,6 +544,7 @@ module Muxr
|
|
|
505
544
|
@parser_params = +""
|
|
506
545
|
when 0x5d # ]
|
|
507
546
|
@parser_state = :osc
|
|
547
|
+
@parser_osc = +""
|
|
508
548
|
when 0x28, 0x29, 0x2a, 0x2b # ( ) * +
|
|
509
549
|
@parser_state = :charset
|
|
510
550
|
when 0x37 # 7 save cursor
|
|
@@ -699,6 +739,7 @@ module Muxr
|
|
|
699
739
|
cell.fg = @fg
|
|
700
740
|
cell.bg = @bg
|
|
701
741
|
cell.attrs = @attrs
|
|
742
|
+
cell.hyperlink = @current_hyperlink
|
|
702
743
|
if @cursor_col >= @cols - 1
|
|
703
744
|
@autowrap_pending = true
|
|
704
745
|
else
|
|
@@ -1020,6 +1061,7 @@ module Muxr
|
|
|
1020
1061
|
@scroll_top = 0
|
|
1021
1062
|
@scroll_bottom = @rows - 1
|
|
1022
1063
|
@autowrap_pending = false
|
|
1064
|
+
@current_hyperlink = nil
|
|
1023
1065
|
end
|
|
1024
1066
|
end
|
|
1025
1067
|
end
|
data/lib/muxr/version.rb
CHANGED
data/lib/muxr.rb
CHANGED
|
@@ -3,6 +3,7 @@ require_relative "muxr/pty_process"
|
|
|
3
3
|
require_relative "muxr/terminal"
|
|
4
4
|
require_relative "muxr/pane"
|
|
5
5
|
require_relative "muxr/drawer"
|
|
6
|
+
require_relative "muxr/foreground_command"
|
|
6
7
|
require_relative "muxr/layout_manager"
|
|
7
8
|
require_relative "muxr/window"
|
|
8
9
|
require_relative "muxr/session"
|
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.
|
|
4
|
+
version: 0.1.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Roel Bondoc
|
|
@@ -61,6 +61,7 @@ files:
|
|
|
61
61
|
- lib/muxr/command_dispatcher.rb
|
|
62
62
|
- lib/muxr/control_server.rb
|
|
63
63
|
- lib/muxr/drawer.rb
|
|
64
|
+
- lib/muxr/foreground_command.rb
|
|
64
65
|
- lib/muxr/input_handler.rb
|
|
65
66
|
- lib/muxr/key_parser.rb
|
|
66
67
|
- lib/muxr/layout_manager.rb
|