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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 03650b7088c85aa8fbe993dbfe17e7f8aac8334f617c3ff148589964d83bb701
4
- data.tar.gz: 9b878beb1c05f1c83273f3f1c8e51944be7c75b3fdcd13935c96f851a2aa8405
3
+ metadata.gz: 16281336febc64c008a43dcd0419fb9ea796dc49f9a36b9ac16f2e20373f302e
4
+ data.tar.gz: d28305f5833ecd082ab850e981a074b1c936d0a06829fbfa1be8814d67ed4faa
5
5
  SHA512:
6
- metadata.gz: 91ccd01254428f3a2333e3abe94cd3d039598813c484730a4819c938f9f0d733ab611bd9c975314ca5bfb0af07c243c69814ed918fc02315e7652436454170b8
7
- data.tar.gz: c58966f41d4a62978e3bfd7560d72ef132b6fcbdededddf129cf94216d77689b1f1f52c44cf7660cab9780a7ec8f3180fe138a9f2f75f8ec741548595c415665
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
@@ -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
- target.terminal.place_selection_cursor(0, 0)
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
@@ -1,3 +1,3 @@
1
1
  module Muxr
2
- VERSION = "0.1.7"
2
+ VERSION = "0.1.8"
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.7
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roel Bondoc