rubyterm 0.2.4 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 201ce77d71abc653a76c0fb9a51827e13910767e5f5946999853a13b8ab02006
4
- data.tar.gz: 4b57378d7077055158b33aca41b4e365c4f63a60bb95547e2c5e64ffa91e1e75
3
+ metadata.gz: a3ad358ca60e1bd23c01f81186b75826f64f1ba9cb519a149a33d56106561062
4
+ data.tar.gz: e367c640889e4724e4266410735df54fbd89a56536d48e4c94b72c8343bd3ac2
5
5
  SHA512:
6
- metadata.gz: d60d6b223b2819739d0dff9e571b8007a2422c2f38fc5d784e733c3273242037d1833fdea48c15735e72b76ef3487e4c716ed8b1cc10b707cfaddac96e76c511
7
- data.tar.gz: 3c5b38c7f9a928f2f1bfa99b798515bb42a0d455c04d956cac11ec8687bac35adcd92fa96c2c6e6c40c28587957ced61c25c747ab990a526a2961c9d01377824
6
+ metadata.gz: b70f474074dfbcbc6eac034340155de305f4ea8fb8daf43a89c82d88c2e5d18ebcedfb81567d230b62e57c63c82e8226533ecc4d20e994058772224149544678
7
+ data.tar.gz: a98d2d55fda7b4954612b82064dde3b95b58884a2e9ade88c65d75e1475c36982db442795a6e8d5514b61ed7b78e4f701235e9cc995442141ea3364cda14369a
data/lib/bitmapwindow.rb CHANGED
@@ -83,10 +83,14 @@ class BitmapWindow
83
83
  # for now - it does not affect correctness of the text, only its scale.
84
84
  def draw(x, y, str, fg, bg, _lineattrs = nil)
85
85
  fillrect(x, y, str.length * @char_w, @char_h, bg)
86
- str.each_char.with_index do |ch, i|
87
- cp = ch.ord
86
+ cps = str.each_char.map(&:ord)
87
+ cps.each_with_index do |cp, i|
88
88
  next if cp == 32 || cp == CharWidth::WIDE_SPACER # space / wide-glyph tail
89
- blit_glyph(cp, x + i * @char_w, y, fg)
89
+ # A WIDE_SPACER in the next cell means this is a double-width glyph; render
90
+ # it two cells wide so it overflows into the (skipped) spacer cell instead
91
+ # of being shrunk into one.
92
+ cells = cps[i + 1] == CharWidth::WIDE_SPACER ? 2 : 1
93
+ blit_glyph(cp, x + i * @char_w, y, fg, cells)
90
94
  end
91
95
  end
92
96
 
@@ -146,8 +150,8 @@ class BitmapWindow
146
150
  end
147
151
  end
148
152
 
149
- def blit_glyph(codepoint, cx, cy, fg)
150
- g = @cache.glyph(codepoint)
153
+ def blit_glyph(codepoint, cx, cy, fg, cells = 1)
154
+ g = @cache.glyph(codepoint, cells)
151
155
  return unless g
152
156
  if g.color?
153
157
  blit_rgba(g, codepoint, cx, cy)
data/lib/charwidth.rb CHANGED
@@ -11,7 +11,6 @@
11
11
  # * width(cp) -> 1 or 2 cells (wide = CJK/Hangul/Kana/fullwidth + emoji)
12
12
  # * emoji?(cp) -> whether to render in colour (the non-CJK wide blocks)
13
13
  #
14
- # Combining marks (width 0) are deliberately not handled yet (treated as 1).
15
14
  module CharWidth
16
15
  # Codepoint stored in the second cell of a double-width glyph: a blank,
17
16
  # advancing placeholder. The terminal writes it after a wide char so the
@@ -19,6 +18,26 @@ module CharWidth
19
18
  # advancing glyph and the bitmap/virtual backends skip it.
20
19
  WIDE_SPACER = 0
21
20
 
21
+ # Zero-width (combining) codepoints. These modify the preceding base glyph
22
+ # rather than advancing the cursor, so they occupy no column — matching tmux
23
+ # (a lone U+FE0F reports cursor_x=0; a base+U+FE0F sequence stays the base's
24
+ # width). Notably this includes the variation selectors: U+FE0F (VS16, emoji
25
+ # presentation) and U+FE0E (VS15, text presentation) request a presentation
26
+ # for the preceding char but are themselves invisible and zero-width. Treating
27
+ # them as width 1 gave each its own cell, drawing a stray glyph over the next
28
+ # column. Per-script combining marks beyond these blocks are a deferred
29
+ # non-goal (still treated as width 1).
30
+ ZERO = [
31
+ 0x0300..0x036F, # combining diacritical marks
32
+ 0x1AB0..0x1AFF, # combining diacritical marks extended
33
+ 0x1DC0..0x1DFF, # combining diacritical marks supplement
34
+ 0x200B..0x200F, # ZWSP, ZWNJ, ZWJ, LRM, RLM (zero-width format)
35
+ 0x20D0..0x20FF, # combining diacritical marks for symbols
36
+ 0xFE00..0xFE0F, # variation selectors (incl. VS15/VS16)
37
+ 0xFE20..0xFE2F, # combining half marks
38
+ 0xE0100..0xE01EF # variation selectors supplement
39
+ ].freeze
40
+
22
41
  # Wide (two-cell) blocks: CJK, Hangul, Kana, fullwidth forms, and emoji.
23
42
  #
24
43
  # In the BMP symbol zone (Misc Symbols, Dingbats, Misc Symbols & Arrows)
@@ -45,9 +64,16 @@ module CharWidth
45
64
  0x1FA70..0x1FAD6, 0x20000..0x2EBE0, 0x2F800..0x2FA1D, 0x30000..0x3134A
46
65
  ].freeze
47
66
 
48
- # The emoji subset of WIDE (excludes CJK/Hangul/Kana/fullwidth): these render
49
- # in colour when a colour font provides them. Digits, '#', '*' etc. are not
50
- # here, so they stay as ordinary text even though Noto maps colour keycaps.
67
+ # Emoji code blocks (excludes CJK/Hangul/Kana/fullwidth): a codepoint renders
68
+ # in colour only if it is BOTH in one of these blocks AND wide (width 2) —
69
+ # see emoji?. The width-2 gate is what distinguishes emoji-presentation
70
+ # codepoints (⚡ U+26A1, ✨ U+2728 — colour, width 2) from text-presentation
71
+ # dingbats that merely share a block (❤ U+2764, ✔ U+2714, ⚠ U+26A0 — width 1).
72
+ # Both properties derive from the same Unicode Emoji_Presentation flag, so
73
+ # this is not "width deciding colour": rather, colour is the subset of these
74
+ # blocks that is wide enough to render a square colour glyph without spilling
75
+ # into the next cell. Digits, '#', '*' are absent, so they stay ordinary text
76
+ # even though Noto maps colour keycaps.
51
77
  EMOJI = [
52
78
  0x231A..0x232A, 0x23E9..0x23F3, 0x25FD..0x27BF, 0x2B1B..0x2B55,
53
79
  0x1F18E..0x1F19A, 0x1F200..0x1F265, 0x1F300..0x1F5A4, 0x1F5FB..0x1F6FC,
@@ -56,18 +82,22 @@ module CharWidth
56
82
 
57
83
  module_function
58
84
 
59
- # Number of terminal cells a codepoint occupies (1 or 2).
85
+ # Number of terminal cells a codepoint occupies (0, 1 or 2).
60
86
  def width(cp)
61
- return 1 if cp < 0x1100 # fast path: ASCII/Latin and most text
87
+ return 1 if cp < 0x0300 # fast path: ASCII/Latin-1 (no combining below)
88
+ return 0 if in_ranges?(ZERO, cp)
89
+ return 1 if cp < 0x1100 # fast path: most other text
62
90
  in_ranges?(WIDE, cp) ? 2 : 1
63
91
  end
64
92
 
65
93
  def wide?(cp) = width(cp) == 2
66
94
 
67
95
  # Whether a codepoint should render as a colour emoji (when the font has it).
96
+ # Gated to width-2 emoji: a colour glyph is rasterised square (~2 cells wide),
97
+ # so colouring a width-1 cell would overflow and corrupt the next column.
68
98
  def emoji?(cp)
69
99
  return false if cp < 0x231A
70
- in_ranges?(EMOJI, cp)
100
+ in_ranges?(EMOJI, cp) && width(cp) == 2
71
101
  end
72
102
 
73
103
  # Binary search over a sorted array of non-overlapping ranges.
data/lib/rubyterm/app.rb CHANGED
@@ -420,6 +420,25 @@ class RubyTerm
420
420
  @buffer.draw_flush
421
421
  end
422
422
 
423
+ # A line of the scroll region [start..bottom] has scrolled up into
424
+ # history. The selection is stored in *buffer* coordinates (negative rows
425
+ # = scrollback), so the content the user selected now sits one row higher:
426
+ # cells inside the scrolled region - and anything already in scrollback,
427
+ # whose negative index shifts as the history grows - move up by one, while
428
+ # content below the region stays put. Shift the stored selection to match
429
+ # so reapply_selection keeps the highlight pinned to the same text as it
430
+ # scrolls (into scrollback and back), instead of leaving it on whatever
431
+ # rolls into the old screen rows. Called from TrackChanges#scroll_up via
432
+ # the adapter for every line moved into history.
433
+ def shift_selection_for_scroll(start, bottom)
434
+ return unless @select_startpos
435
+ [@select_startpos, @select_endpos].each do |pos|
436
+ next unless pos
437
+ y = pos[1]
438
+ pos[1] -= 1 if y < 0 || (y >= start && y <= bottom)
439
+ end
440
+ end
441
+
423
442
  # FIXME: Cursor, selection etc. are "special" overlays on top of attributes.
424
443
  # Allow the terminal to set a set of positions + fg/bg, and a set of ranges.
425
444
  def render_selection
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class RubyTerm
4
- VERSION = "0.2.4"
4
+ VERSION = "0.2.6"
5
5
  end
data/lib/term.rb CHANGED
@@ -565,6 +565,11 @@ class Term
565
565
  return if ch == 127 # DEL is ignored in the data stream
566
566
 
567
567
  cw = CharWidth.width(ch)
568
+ # Zero-width codepoints (variation selectors like U+FE0F, combining marks,
569
+ # zero-width joiners) modify the preceding glyph. This buffer stores one
570
+ # codepoint per cell and can't compose, so drop them rather than letting
571
+ # them claim a cell of their own — which would paint over the next column.
572
+ return if cw == 0
568
573
  # A double-width glyph can't straddle the right margin: if only one
569
574
  # column is left, blank it and wrap so the glyph starts the next line.
570
575
  if cw == 2 && @wraparound && @x == line_width - 1
data/lib/trackchanges.rb CHANGED
@@ -69,6 +69,10 @@ class TrackChanges
69
69
  start = @buffer.scroll_start.to_i
70
70
  bottom = @buffer.scroll_end || (@rows - 1)
71
71
  @buffer.scroll_up
72
+ # The content moved up in the buffer; keep any active text selection
73
+ # pinned to it. Done before the @suspend bail-out so the selection stays
74
+ # aligned even through jump-scrolled (unrendered) frames.
75
+ @adapter.scroll_selection(start, bottom)
72
76
  return if @suspend
73
77
  if @adapter.scrollback_mode
74
78
  @adapter.scrollback_anchor
data/lib/windowadapter.rb CHANGED
@@ -30,6 +30,14 @@ class WindowAdapter
30
30
  # offset instead of letting the viewport drift.
31
31
  def scrollback_anchor = @window.scrollback_anchor
32
32
 
33
+ # A line of the scroll region [start..bottom] has moved into history.
34
+ # Forward to the orchestrator so it can keep a live text selection pinned
35
+ # to the content it covers. The headless harness/bench hosts have no
36
+ # selection state and do not implement this, so it no-ops there.
37
+ def scroll_selection(start, bottom)
38
+ @term.shift_selection_for_scroll(start, bottom) if @term.respond_to?(:shift_selection_for_scroll)
39
+ end
40
+
33
41
  # DECCOLM: the terminal asked to switch to +cols+ columns (80/132). The
34
42
  # orchestrator (RubyTerm) owns the window pixels, font scale, config and
35
43
  # the pty size report, so delegate to it. The headless harness "term"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubyterm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.2.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vidar Hokstad
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-26 00:00:00.000000000 Z
11
+ date: 2026-06-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pure-x11