muxr 0.1.7 → 0.1.8
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 +15 -0
- data/lib/muxr/application.rb +4 -2
- data/lib/muxr/terminal.rb +71 -0
- 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: 16281336febc64c008a43dcd0419fb9ea796dc49f9a36b9ac16f2e20373f302e
|
|
4
|
+
data.tar.gz: d28305f5833ecd082ab850e981a074b1c936d0a06829fbfa1be8814d67ed4faa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a31cf649a48541e25b417ab92f9ec14cda5d64acfd0827532792d181effc6b051d5c97234f6fe734ffc97c951819caf857da43a2902ec7068d4f0a5b798c1e40
|
|
7
|
+
data.tar.gz: 22b1e5990231d697960c9fe8ef7e7d8c4a6502eb8305497dc5759b8cd5dab785f38f21be92ab835a69afd194b74f1eaf45b184332b64cb5c1d7e97545e937ba1
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,21 @@ follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.8] - 2026-05-22
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Wrapped plain-text URLs are stamped with a shared OSC 8 hyperlink id
|
|
13
|
+
after each `Terminal#feed`. The Terminal scans the live buffer plus
|
|
14
|
+
the last scrollback row for `http`/`https`/`ftp` URLs and tags the
|
|
15
|
+
covering cells with `id=muxr-url-<hash>` so Ghostty / iTerm2 / kitty /
|
|
16
|
+
WezTerm merge the wrapped halves into a single clickable link.
|
|
17
|
+
Program-emitted OSC 8 payloads continue to pass through unchanged.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- Selection mode now anchors at the live cursor's visible position
|
|
21
|
+
instead of `(0,0)`, so visual selection starts where the user's
|
|
22
|
+
attention already is.
|
|
23
|
+
|
|
9
24
|
## [0.1.7] - 2026-05-20
|
|
10
25
|
|
|
11
26
|
### Added
|
data/lib/muxr/application.rb
CHANGED
|
@@ -473,8 +473,10 @@ module Muxr
|
|
|
473
473
|
return unless target
|
|
474
474
|
# Vim-style: drop the user at a movable cursor with NO selection yet.
|
|
475
475
|
# They navigate with h/j/k/l, then press v (linear) or C-v (block) to
|
|
476
|
-
# anchor.
|
|
477
|
-
|
|
476
|
+
# anchor. Start at the live cursor's visible position so the user lands
|
|
477
|
+
# where their attention already is, instead of the top-left corner.
|
|
478
|
+
term = target.terminal
|
|
479
|
+
term.place_selection_cursor(term.cursor_row, term.cursor_col)
|
|
478
480
|
@input.enter_selection_mode
|
|
479
481
|
@renderer.reset_frame!
|
|
480
482
|
invalidate
|
data/lib/muxr/terminal.rb
CHANGED
|
@@ -17,6 +17,22 @@ module Muxr
|
|
|
17
17
|
# tolerant of weird inputs without giving an attacker an unbounded sink.
|
|
18
18
|
OSC_MAX_LEN = 4096
|
|
19
19
|
|
|
20
|
+
# Match plain-text URLs the inner program printed without wrapping them
|
|
21
|
+
# in OSC 8. We stamp the matching cells with a synthetic hyperlink so the
|
|
22
|
+
# outer terminal treats a wrapped URL as one click target instead of two
|
|
23
|
+
# truncated halves. The character class excludes whitespace, control
|
|
24
|
+
# bytes, and the punctuation that almost never sits inside a URL.
|
|
25
|
+
URL_REGEX = %r{(?:https?|ftp)://[^\s<>"\\^`\x00-\x1f\x7f]+}
|
|
26
|
+
# Trailing punctuation we trim from a detected URL — these usually belong
|
|
27
|
+
# to the surrounding sentence ("see https://x.com.") rather than the URL
|
|
28
|
+
# itself. Parens/brackets are intentionally left alone since they're
|
|
29
|
+
# commonly part of Wikipedia-style URLs.
|
|
30
|
+
URL_TRIM_TRAILING = ".,;:!?'\""
|
|
31
|
+
# Prefix on the OSC 8 payload of cells we tagged ourselves. Used to tell
|
|
32
|
+
# synthetic links apart from program-emitted ones so we never clobber
|
|
33
|
+
# OSC 8 links the inner program set.
|
|
34
|
+
SYNTH_URL_PREFIX = "8;id=muxr-url-"
|
|
35
|
+
|
|
20
36
|
# Inner programs (fzf ≥ 0.41, neovim, helix, …) bracket coherent screen
|
|
21
37
|
# updates with `\e[?2026h … \e[?2026l` (DECSET 2026 — "Synchronized
|
|
22
38
|
# Output"). When we see the open, we know more bytes are coming that
|
|
@@ -68,6 +84,10 @@ module Muxr
|
|
|
68
84
|
# links share one frozen string for fast equality and small memory.
|
|
69
85
|
@current_hyperlink = nil
|
|
70
86
|
@hyperlink_intern = {}
|
|
87
|
+
# Stable interning for synthetic URL hyperlinks. Keyed by the URI text
|
|
88
|
+
# so the same URL produces the same payload string across scans —
|
|
89
|
+
# otherwise every feed would churn the renderer diff.
|
|
90
|
+
@synth_url_intern = {}
|
|
71
91
|
@dirty = true
|
|
72
92
|
@scrollback = []
|
|
73
93
|
@view_offset = 0
|
|
@@ -535,9 +555,60 @@ module Muxr
|
|
|
535
555
|
return if str.empty?
|
|
536
556
|
end
|
|
537
557
|
str.each_char { |c| process_char(c) }
|
|
558
|
+
detect_urls!
|
|
538
559
|
@dirty = true
|
|
539
560
|
end
|
|
540
561
|
|
|
562
|
+
# Walk the buffer (plus the last scrollback row so wraps across the
|
|
563
|
+
# scrollback boundary still join), find plain-text URLs, and stamp the
|
|
564
|
+
# covering cells with an OSC 8 hyperlink carrying an `id=` parameter.
|
|
565
|
+
# Outer terminals (Ghostty, iTerm2, kitty, WezTerm) use `id=` to merge
|
|
566
|
+
# spans that wrap across rows into a single click target — without this
|
|
567
|
+
# a wrapped URL like https://very.long.example.com/path-that-wraps would
|
|
568
|
+
# be detected as two truncated URLs on consecutive lines.
|
|
569
|
+
def detect_urls!
|
|
570
|
+
rows = []
|
|
571
|
+
rows << @scrollback.last if @scrollback.any?
|
|
572
|
+
rows.concat(@buffer)
|
|
573
|
+
|
|
574
|
+
rows.each do |row|
|
|
575
|
+
row.each do |cell|
|
|
576
|
+
link = cell.hyperlink
|
|
577
|
+
cell.hyperlink = nil if link && link.start_with?(SYNTH_URL_PREFIX)
|
|
578
|
+
end
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
text = String.new(capacity: rows.length * @cols)
|
|
582
|
+
cells = []
|
|
583
|
+
rows.each do |row|
|
|
584
|
+
row.each do |cell|
|
|
585
|
+
text << cell.char
|
|
586
|
+
cells << cell
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
pos = 0
|
|
591
|
+
while (md = URL_REGEX.match(text, pos))
|
|
592
|
+
start_off = md.begin(0)
|
|
593
|
+
end_off = md.end(0)
|
|
594
|
+
while end_off > start_off + 1 && URL_TRIM_TRAILING.include?(text[end_off - 1])
|
|
595
|
+
end_off -= 1
|
|
596
|
+
end
|
|
597
|
+
uri = text[start_off...end_off]
|
|
598
|
+
payload = (@synth_url_intern[uri] ||=
|
|
599
|
+
"#{SYNTH_URL_PREFIX}#{uri.hash.abs.to_s(16)};#{uri}".freeze)
|
|
600
|
+
|
|
601
|
+
(start_off...end_off).each do |off|
|
|
602
|
+
cell = cells[off]
|
|
603
|
+
existing = cell.hyperlink
|
|
604
|
+
next if existing && !existing.start_with?(SYNTH_URL_PREFIX)
|
|
605
|
+
cell.hyperlink = payload
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
pos = end_off
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
|
|
541
612
|
private
|
|
542
613
|
|
|
543
614
|
def blank_cell
|
data/lib/muxr/version.rb
CHANGED