r2ui 0.1.0 → 0.2.0

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.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +64 -0
  3. data/README.md +79 -0
  4. data/lib/r2ui/app.rb +311 -34
  5. data/lib/r2ui/canvas.rb +135 -14
  6. data/lib/r2ui/cli/builder.rb +72 -0
  7. data/lib/r2ui/cli/context.rb +45 -0
  8. data/lib/r2ui/cli/definition.rb +174 -0
  9. data/lib/r2ui/cli/ext/ask.rb +133 -0
  10. data/lib/r2ui/cli/ext/box.rb +112 -0
  11. data/lib/r2ui/cli/ext/choose.rb +162 -0
  12. data/lib/r2ui/cli/ext/choose_many.rb +241 -0
  13. data/lib/r2ui/cli/ext/color_option.rb +64 -0
  14. data/lib/r2ui/cli/ext/completion.rb +520 -0
  15. data/lib/r2ui/cli/ext/config.rb +179 -0
  16. data/lib/r2ui/cli/ext/confirm.rb +110 -0
  17. data/lib/r2ui/cli/ext/dashboard.rb +105 -0
  18. data/lib/r2ui/cli/ext/diff.rb +195 -0
  19. data/lib/r2ui/cli/ext/editor.rb +94 -0
  20. data/lib/r2ui/cli/ext/env.rb +108 -0
  21. data/lib/r2ui/cli/ext/examples.rb +62 -0
  22. data/lib/r2ui/cli/ext/filter.rb +269 -0
  23. data/lib/r2ui/cli/ext/format.rb +104 -0
  24. data/lib/r2ui/cli/ext/list.rb +117 -0
  25. data/lib/r2ui/cli/ext/log.rb +89 -0
  26. data/lib/r2ui/cli/ext/pager.rb +97 -0
  27. data/lib/r2ui/cli/ext/pairs.rb +62 -0
  28. data/lib/r2ui/cli/ext/parallel.rb +274 -0
  29. data/lib/r2ui/cli/ext/password.rb +158 -0
  30. data/lib/r2ui/cli/ext/progress.rb +330 -0
  31. data/lib/r2ui/cli/ext/reference.rb +129 -0
  32. data/lib/r2ui/cli/ext/spinner.rb +165 -0
  33. data/lib/r2ui/cli/ext/summary.rb +63 -0
  34. data/lib/r2ui/cli/ext/table.rb +141 -0
  35. data/lib/r2ui/cli/ext/tasks.rb +216 -0
  36. data/lib/r2ui/cli/ext/trace.rb +64 -0
  37. data/lib/r2ui/cli/ext/tree.rb +114 -0
  38. data/lib/r2ui/cli/ext/version.rb +67 -0
  39. data/lib/r2ui/cli/extension.rb +129 -0
  40. data/lib/r2ui/cli/help.rb +115 -0
  41. data/lib/r2ui/cli/helpers.rb +20 -0
  42. data/lib/r2ui/cli/live.rb +150 -0
  43. data/lib/r2ui/cli/parser.rb +165 -0
  44. data/lib/r2ui/cli/prompt.rb +133 -0
  45. data/lib/r2ui/cli/runner.rb +86 -0
  46. data/lib/r2ui/cli/shell.rb +185 -0
  47. data/lib/r2ui/cli/testing.rb +49 -0
  48. data/lib/r2ui/cli.rb +129 -0
  49. data/lib/r2ui/compat/bubbletea/LICENSE-bubbletea.txt +21 -0
  50. data/lib/r2ui/compat/bubbletea/ansi.rb +424 -0
  51. data/lib/r2ui/compat/bubbletea/commands.rb +143 -0
  52. data/lib/r2ui/compat/bubbletea/input.rb +269 -0
  53. data/lib/r2ui/compat/bubbletea/input_reader.rb +66 -0
  54. data/lib/r2ui/compat/bubbletea/messages.rb +251 -0
  55. data/lib/r2ui/compat/bubbletea/model.rb +65 -0
  56. data/lib/r2ui/compat/bubbletea/native.rb +254 -0
  57. data/lib/r2ui/compat/bubbletea/renderer.rb +192 -0
  58. data/lib/r2ui/compat/bubbletea/runner.rb +425 -0
  59. data/lib/r2ui/compat/bubbletea/terminal.rb +230 -0
  60. data/lib/r2ui/compat/bubbletea/version.rb +8 -0
  61. data/lib/r2ui/compat/bubbletea/width_table.rb +228 -0
  62. data/lib/r2ui/compat/bubbletea.rb +16 -0
  63. data/lib/r2ui/compat/lipgloss/border.rb +17 -0
  64. data/lib/r2ui/compat/lipgloss/color.rb +78 -0
  65. data/lib/r2ui/compat/lipgloss/color_blend.rb +166 -0
  66. data/lib/r2ui/compat/lipgloss/colorful.rb +585 -0
  67. data/lib/r2ui/compat/lipgloss/layout.rb +461 -0
  68. data/lib/r2ui/compat/lipgloss/list.rb +85 -0
  69. data/lib/r2ui/compat/lipgloss/position.rb +30 -0
  70. data/lib/r2ui/compat/lipgloss/style/border.rb +134 -0
  71. data/lib/r2ui/compat/lipgloss/style.rb +1062 -0
  72. data/lib/r2ui/compat/lipgloss/table/render.rb +518 -0
  73. data/lib/r2ui/compat/lipgloss/table.rb +185 -0
  74. data/lib/r2ui/compat/lipgloss/termenv.rb +768 -0
  75. data/lib/r2ui/compat/lipgloss/text/unicode_tables.rb +428 -0
  76. data/lib/r2ui/compat/lipgloss/text.rb +1555 -0
  77. data/lib/r2ui/compat/lipgloss/tree/go_tree.rb +243 -0
  78. data/lib/r2ui/compat/lipgloss/tree.rb +105 -0
  79. data/lib/r2ui/compat/lipgloss/version.rb +15 -0
  80. data/lib/r2ui/compat/lipgloss.rb +64 -0
  81. data/lib/r2ui/compat/load_path/bubbletea.rb +4 -0
  82. data/lib/r2ui/compat/load_path/lipgloss.rb +4 -0
  83. data/lib/r2ui/component.rb +67 -0
  84. data/lib/r2ui/context.rb +87 -0
  85. data/lib/r2ui/drop_in.rb +16 -0
  86. data/lib/r2ui/dsl/column.rb +11 -3
  87. data/lib/r2ui/dsl/dashboard.rb +45 -9
  88. data/lib/r2ui/dsl/resource.rb +12 -0
  89. data/lib/r2ui/ext/after.rb +54 -0
  90. data/lib/r2ui/ext/async.rb +81 -0
  91. data/lib/r2ui/ext/batch_sequence.rb +54 -0
  92. data/lib/r2ui/ext/countdown.rb +52 -0
  93. data/lib/r2ui/ext/data_table.rb +71 -0
  94. data/lib/r2ui/ext/every.rb +66 -0
  95. data/lib/r2ui/ext/exec.rb +116 -0
  96. data/lib/r2ui/ext/file_picker.rb +78 -0
  97. data/lib/r2ui/ext/focus_report.rb +69 -0
  98. data/lib/r2ui/ext/help.rb +62 -0
  99. data/lib/r2ui/ext/keys.rb +64 -0
  100. data/lib/r2ui/ext/list.rb +145 -0
  101. data/lib/r2ui/ext/messages.rb +87 -0
  102. data/lib/r2ui/ext/mouse.rb +134 -0
  103. data/lib/r2ui/ext/paginator.rb +86 -0
  104. data/lib/r2ui/ext/paste.rb +86 -0
  105. data/lib/r2ui/ext/println.rb +21 -0
  106. data/lib/r2ui/ext/progress.rb +85 -0
  107. data/lib/r2ui/ext/refresh.rb +56 -0
  108. data/lib/r2ui/ext/resize.rb +62 -0
  109. data/lib/r2ui/ext/screen.rb +47 -0
  110. data/lib/r2ui/ext/screen_mode.rb +63 -0
  111. data/lib/r2ui/ext/spinner.rb +59 -0
  112. data/lib/r2ui/ext/state.rb +45 -0
  113. data/lib/r2ui/ext/stopwatch.rb +67 -0
  114. data/lib/r2ui/ext/suspend.rb +56 -0
  115. data/lib/r2ui/ext/text_area.rb +73 -0
  116. data/lib/r2ui/ext/text_input.rb +83 -0
  117. data/lib/r2ui/ext/theme.rb +112 -0
  118. data/lib/r2ui/ext/view.rb +37 -0
  119. data/lib/r2ui/ext/viewport.rb +92 -0
  120. data/lib/r2ui/ext/window_title.rb +51 -0
  121. data/lib/r2ui/extension.rb +199 -0
  122. data/lib/r2ui/history.rb +4 -1
  123. data/lib/r2ui/keys.rb +35 -8
  124. data/lib/r2ui/motion.rb +169 -0
  125. data/lib/r2ui/panel_state.rb +46 -2
  126. data/lib/r2ui/renderer.rb +87 -22
  127. data/lib/r2ui/version.rb +1 -1
  128. data/lib/r2ui/widgets/box.rb +6 -3
  129. data/lib/r2ui/widgets/glyphs.rb +149 -0
  130. data/lib/r2ui/widgets/table.rb +109 -25
  131. data/lib/r2ui.rb +12 -1
  132. metadata +119 -4
  133. data/lib/r2ui/terminal.rb +0 -53
@@ -0,0 +1,1555 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "text/unicode_tables"
4
+
5
+ module R2UI
6
+ module Compat
7
+ module Gloss
8
+ # ANSI-aware text utilities: byte-for-byte ports of what lipgloss v1.1.0 uses from
9
+ # charmbracelet/x/ansi v0.8.0 (StringWidth, Strip, Truncate, TruncateLeft, Cut, Wrap, Hardwrap,
10
+ # Wordwrap, DecodeSequence), charmbracelet/x/cellbuf (Wrap, ReadStyle, ReadLink, Style#Sequence)
11
+ # and rivo/uniseg v0.4.7 (FirstGraphemeCluster and runeWidth).
12
+ #
13
+ # Every function works on the string's bytes exactly like the Go code, so ANSI escape sequences,
14
+ # invalid UTF-8 and C1 control bytes are handled the same way. Results are UTF-8 strings.
15
+ module Text
16
+ # --- DEC ANSI parser transition table (x/ansi/parser/transition_table.go) ---------------------
17
+
18
+ GROUND = 0
19
+ CSI_ENTRY = 1
20
+ CSI_INTERMEDIATE = 2
21
+ CSI_PARAM = 3
22
+ DCS_ENTRY = 4
23
+ DCS_INTERMEDIATE = 5
24
+ DCS_PARAM = 6
25
+ DCS_STRING = 7
26
+ ESCAPE = 8
27
+ ESCAPE_INTERMEDIATE = 9
28
+ OSC_STRING = 10
29
+ SOS_STRING = 11
30
+ PM_STRING = 12
31
+ APC_STRING = 13
32
+ UTF8 = 14
33
+
34
+ A_NONE = 0
35
+ A_CLEAR = 1
36
+ A_COLLECT = 2
37
+ A_PREFIX = 3
38
+ A_DISPATCH = 4
39
+ A_EXECUTE = 5
40
+ A_START = 6
41
+ A_PUT = 7
42
+ A_PARAM = 8
43
+ A_PRINT = 9
44
+
45
+ # Index: state << 8 | byte. Value: action << 4 | next state.
46
+ TABLE = begin
47
+ t = Array.new(4096, (A_NONE << 4) | GROUND)
48
+ one = ->(code, state, action, nxt) { t[(state << 8) | code] = (action << 4) | nxt }
49
+ rng = ->(from, to, state, action, nxt) { (from..to).each { |c| one.(c, state, action, nxt) } }
50
+
51
+ (GROUND..UTF8).each do |st|
52
+ [0x18, 0x1a, 0x99, 0x9a].each { |c| one.(c, st, A_EXECUTE, GROUND) }
53
+ rng.(0x80, 0x8F, st, A_EXECUTE, GROUND)
54
+ rng.(0x90, 0x97, st, A_EXECUTE, GROUND)
55
+ one.(0x9C, st, A_EXECUTE, GROUND)
56
+ one.(0x1B, st, A_CLEAR, ESCAPE)
57
+ one.(0x98, st, A_START, SOS_STRING)
58
+ one.(0x9E, st, A_START, PM_STRING)
59
+ one.(0x9F, st, A_START, APC_STRING)
60
+ one.(0x9B, st, A_CLEAR, CSI_ENTRY)
61
+ one.(0x90, st, A_CLEAR, DCS_ENTRY)
62
+ one.(0x9D, st, A_START, OSC_STRING)
63
+ rng.(0xC2, 0xDF, st, A_COLLECT, UTF8)
64
+ rng.(0xE0, 0xEF, st, A_COLLECT, UTF8)
65
+ rng.(0xF0, 0xF4, st, A_COLLECT, UTF8)
66
+ end
67
+
68
+ rng.(0x00, 0x17, GROUND, A_EXECUTE, GROUND)
69
+ one.(0x19, GROUND, A_EXECUTE, GROUND)
70
+ rng.(0x1C, 0x1F, GROUND, A_EXECUTE, GROUND)
71
+ rng.(0x20, 0x7E, GROUND, A_PRINT, GROUND)
72
+ one.(0x7F, GROUND, A_EXECUTE, GROUND)
73
+
74
+ rng.(0x00, 0x17, ESCAPE_INTERMEDIATE, A_EXECUTE, ESCAPE_INTERMEDIATE)
75
+ one.(0x19, ESCAPE_INTERMEDIATE, A_EXECUTE, ESCAPE_INTERMEDIATE)
76
+ rng.(0x1C, 0x1F, ESCAPE_INTERMEDIATE, A_EXECUTE, ESCAPE_INTERMEDIATE)
77
+ rng.(0x20, 0x2F, ESCAPE_INTERMEDIATE, A_COLLECT, ESCAPE_INTERMEDIATE)
78
+ one.(0x7F, ESCAPE_INTERMEDIATE, A_NONE, ESCAPE_INTERMEDIATE)
79
+ rng.(0x30, 0x7E, ESCAPE_INTERMEDIATE, A_DISPATCH, GROUND)
80
+
81
+ rng.(0x00, 0x17, ESCAPE, A_EXECUTE, ESCAPE)
82
+ one.(0x19, ESCAPE, A_EXECUTE, ESCAPE)
83
+ rng.(0x1C, 0x1F, ESCAPE, A_EXECUTE, ESCAPE)
84
+ one.(0x7F, ESCAPE, A_NONE, ESCAPE)
85
+ rng.(0x30, 0x4F, ESCAPE, A_DISPATCH, GROUND)
86
+ rng.(0x51, 0x57, ESCAPE, A_DISPATCH, GROUND)
87
+ one.(0x59, ESCAPE, A_DISPATCH, GROUND)
88
+ one.(0x5A, ESCAPE, A_DISPATCH, GROUND)
89
+ one.(0x5C, ESCAPE, A_DISPATCH, GROUND)
90
+ rng.(0x60, 0x7E, ESCAPE, A_DISPATCH, GROUND)
91
+ rng.(0x20, 0x2F, ESCAPE, A_COLLECT, ESCAPE_INTERMEDIATE)
92
+ one.(0x58, ESCAPE, A_START, SOS_STRING)
93
+ one.(0x5E, ESCAPE, A_START, PM_STRING)
94
+ one.(0x5F, ESCAPE, A_START, APC_STRING)
95
+ one.(0x50, ESCAPE, A_CLEAR, DCS_ENTRY)
96
+ one.(0x5B, ESCAPE, A_CLEAR, CSI_ENTRY)
97
+ one.(0x5D, ESCAPE, A_START, OSC_STRING)
98
+
99
+ (SOS_STRING..APC_STRING).each do |st|
100
+ rng.(0x00, 0x17, st, A_PUT, st)
101
+ one.(0x19, st, A_PUT, st)
102
+ rng.(0x1C, 0x1F, st, A_PUT, st)
103
+ rng.(0x20, 0x7F, st, A_PUT, st)
104
+ one.(0x1B, st, A_DISPATCH, ESCAPE)
105
+ one.(0x9C, st, A_DISPATCH, GROUND)
106
+ [0x18, 0x1A].each { |c| one.(c, st, A_NONE, GROUND) }
107
+ end
108
+
109
+ rng.(0x00, 0x07, DCS_ENTRY, A_NONE, DCS_ENTRY)
110
+ rng.(0x0E, 0x17, DCS_ENTRY, A_NONE, DCS_ENTRY)
111
+ one.(0x19, DCS_ENTRY, A_NONE, DCS_ENTRY)
112
+ rng.(0x1C, 0x1F, DCS_ENTRY, A_NONE, DCS_ENTRY)
113
+ one.(0x7F, DCS_ENTRY, A_NONE, DCS_ENTRY)
114
+ rng.(0x20, 0x2F, DCS_ENTRY, A_COLLECT, DCS_INTERMEDIATE)
115
+ rng.(0x30, 0x3B, DCS_ENTRY, A_PARAM, DCS_PARAM)
116
+ rng.(0x3C, 0x3F, DCS_ENTRY, A_PREFIX, DCS_PARAM)
117
+ rng.(0x08, 0x0D, DCS_ENTRY, A_PUT, DCS_STRING)
118
+ one.(0x1B, DCS_ENTRY, A_PUT, DCS_STRING)
119
+ rng.(0x40, 0x7E, DCS_ENTRY, A_START, DCS_STRING)
120
+
121
+ rng.(0x00, 0x17, DCS_INTERMEDIATE, A_NONE, DCS_INTERMEDIATE)
122
+ one.(0x19, DCS_INTERMEDIATE, A_NONE, DCS_INTERMEDIATE)
123
+ rng.(0x1C, 0x1F, DCS_INTERMEDIATE, A_NONE, DCS_INTERMEDIATE)
124
+ rng.(0x20, 0x2F, DCS_INTERMEDIATE, A_COLLECT, DCS_INTERMEDIATE)
125
+ one.(0x7F, DCS_INTERMEDIATE, A_NONE, DCS_INTERMEDIATE)
126
+ rng.(0x30, 0x3F, DCS_INTERMEDIATE, A_START, DCS_STRING)
127
+ rng.(0x40, 0x7E, DCS_INTERMEDIATE, A_START, DCS_STRING)
128
+
129
+ rng.(0x00, 0x17, DCS_PARAM, A_NONE, DCS_PARAM)
130
+ one.(0x19, DCS_PARAM, A_NONE, DCS_PARAM)
131
+ rng.(0x1C, 0x1F, DCS_PARAM, A_NONE, DCS_PARAM)
132
+ rng.(0x30, 0x3B, DCS_PARAM, A_PARAM, DCS_PARAM)
133
+ one.(0x7F, DCS_PARAM, A_NONE, DCS_PARAM)
134
+ rng.(0x3C, 0x3F, DCS_PARAM, A_NONE, DCS_PARAM)
135
+ rng.(0x20, 0x2F, DCS_PARAM, A_COLLECT, DCS_INTERMEDIATE)
136
+ rng.(0x40, 0x7E, DCS_PARAM, A_START, DCS_STRING)
137
+
138
+ rng.(0x00, 0x17, DCS_STRING, A_PUT, DCS_STRING)
139
+ one.(0x19, DCS_STRING, A_PUT, DCS_STRING)
140
+ rng.(0x1C, 0x1F, DCS_STRING, A_PUT, DCS_STRING)
141
+ rng.(0x20, 0x7E, DCS_STRING, A_PUT, DCS_STRING)
142
+ one.(0x7F, DCS_STRING, A_PUT, DCS_STRING)
143
+ rng.(0x80, 0xFF, DCS_STRING, A_PUT, DCS_STRING)
144
+ one.(0x1B, DCS_STRING, A_DISPATCH, ESCAPE)
145
+ one.(0x9C, DCS_STRING, A_DISPATCH, GROUND)
146
+ [0x18, 0x1A].each { |c| one.(c, DCS_STRING, A_NONE, GROUND) }
147
+
148
+ rng.(0x00, 0x17, CSI_PARAM, A_EXECUTE, CSI_PARAM)
149
+ one.(0x19, CSI_PARAM, A_EXECUTE, CSI_PARAM)
150
+ rng.(0x1C, 0x1F, CSI_PARAM, A_EXECUTE, CSI_PARAM)
151
+ rng.(0x30, 0x3B, CSI_PARAM, A_PARAM, CSI_PARAM)
152
+ one.(0x7F, CSI_PARAM, A_NONE, CSI_PARAM)
153
+ rng.(0x3C, 0x3F, CSI_PARAM, A_NONE, CSI_PARAM)
154
+ rng.(0x40, 0x7E, CSI_PARAM, A_DISPATCH, GROUND)
155
+ rng.(0x20, 0x2F, CSI_PARAM, A_COLLECT, CSI_INTERMEDIATE)
156
+
157
+ rng.(0x00, 0x17, CSI_INTERMEDIATE, A_EXECUTE, CSI_INTERMEDIATE)
158
+ one.(0x19, CSI_INTERMEDIATE, A_EXECUTE, CSI_INTERMEDIATE)
159
+ rng.(0x1C, 0x1F, CSI_INTERMEDIATE, A_EXECUTE, CSI_INTERMEDIATE)
160
+ rng.(0x20, 0x2F, CSI_INTERMEDIATE, A_COLLECT, CSI_INTERMEDIATE)
161
+ one.(0x7F, CSI_INTERMEDIATE, A_NONE, CSI_INTERMEDIATE)
162
+ rng.(0x40, 0x7E, CSI_INTERMEDIATE, A_DISPATCH, GROUND)
163
+ rng.(0x30, 0x3F, CSI_INTERMEDIATE, A_NONE, GROUND)
164
+
165
+ rng.(0x00, 0x17, CSI_ENTRY, A_EXECUTE, CSI_ENTRY)
166
+ one.(0x19, CSI_ENTRY, A_EXECUTE, CSI_ENTRY)
167
+ rng.(0x1C, 0x1F, CSI_ENTRY, A_EXECUTE, CSI_ENTRY)
168
+ one.(0x7F, CSI_ENTRY, A_NONE, CSI_ENTRY)
169
+ rng.(0x40, 0x7E, CSI_ENTRY, A_DISPATCH, GROUND)
170
+ rng.(0x20, 0x2F, CSI_ENTRY, A_COLLECT, CSI_INTERMEDIATE)
171
+ rng.(0x30, 0x3B, CSI_ENTRY, A_PARAM, CSI_PARAM)
172
+ rng.(0x3C, 0x3F, CSI_ENTRY, A_PREFIX, CSI_PARAM)
173
+
174
+ rng.(0x00, 0x06, OSC_STRING, A_NONE, OSC_STRING)
175
+ rng.(0x08, 0x17, OSC_STRING, A_NONE, OSC_STRING)
176
+ one.(0x19, OSC_STRING, A_NONE, OSC_STRING)
177
+ rng.(0x1C, 0x1F, OSC_STRING, A_NONE, OSC_STRING)
178
+ rng.(0x20, 0xFF, OSC_STRING, A_PUT, OSC_STRING)
179
+ one.(0x1B, OSC_STRING, A_DISPATCH, ESCAPE)
180
+ one.(0x07, OSC_STRING, A_DISPATCH, GROUND)
181
+ one.(0x9C, OSC_STRING, A_DISPATCH, GROUND)
182
+ [0x18, 0x1A].each { |c| one.(c, OSC_STRING, A_NONE, GROUND) }
183
+
184
+ t.freeze
185
+ end
186
+
187
+ # --- uniseg grapheme clusters and widths (grapheme.go, graphemerules.go, width.go) -----------
188
+
189
+ PR_ANY = 1
190
+ PR_PREPEND = 2
191
+ PR_CR = 3
192
+ PR_LF = 4
193
+ PR_CONTROL = 5
194
+ PR_EXTEND = 6
195
+ PR_REGIONAL_INDICATOR = 7
196
+ PR_SPACING_MARK = 8
197
+ PR_L = 9
198
+ PR_V = 10
199
+ PR_T = 11
200
+ PR_LV = 12
201
+ PR_LVT = 13
202
+ PR_ZWJ = 14
203
+ PR_EXTENDED_PICTOGRAPHIC = 15
204
+
205
+ GR_ANY = 0
206
+ GR_CR = 1
207
+ GR_CONTROL_LF = 2
208
+ GR_L = 3
209
+ GR_LVV = 4
210
+ GR_LVTT = 5
211
+ GR_PREPEND = 6
212
+ GR_EXTENDED_PICTOGRAPHIC = 7
213
+ GR_EXTENDED_PICTOGRAPHIC_ZWJ = 8
214
+ GR_RI_ODD = 9
215
+ GR_RI_EVEN = 10
216
+
217
+ RUNE_ERROR = 0xFFFD
218
+ VS15 = 0xFE0E
219
+ VS16 = 0xFE0F
220
+
221
+ # grTransitions: [state, prop] => [new state, boundary?, rule number].
222
+ GR_TRANSITIONS = {
223
+ [GR_ANY, PR_CR] => [GR_CR, true, 50],
224
+ [GR_ANY, PR_LF] => [GR_CONTROL_LF, true, 50],
225
+ [GR_ANY, PR_CONTROL] => [GR_CONTROL_LF, true, 50],
226
+ [GR_CR, PR_ANY] => [GR_ANY, true, 40],
227
+ [GR_CONTROL_LF, PR_ANY] => [GR_ANY, true, 40],
228
+ [GR_CR, PR_LF] => [GR_CONTROL_LF, false, 30],
229
+ [GR_ANY, PR_L] => [GR_L, true, 9990],
230
+ [GR_L, PR_L] => [GR_L, false, 60],
231
+ [GR_L, PR_V] => [GR_LVV, false, 60],
232
+ [GR_L, PR_LV] => [GR_LVV, false, 60],
233
+ [GR_L, PR_LVT] => [GR_LVTT, false, 60],
234
+ [GR_ANY, PR_LV] => [GR_LVV, true, 9990],
235
+ [GR_ANY, PR_V] => [GR_LVV, true, 9990],
236
+ [GR_LVV, PR_V] => [GR_LVV, false, 70],
237
+ [GR_LVV, PR_T] => [GR_LVTT, false, 70],
238
+ [GR_ANY, PR_LVT] => [GR_LVTT, true, 9990],
239
+ [GR_ANY, PR_T] => [GR_LVTT, true, 9990],
240
+ [GR_LVTT, PR_T] => [GR_LVTT, false, 80],
241
+ [GR_ANY, PR_EXTEND] => [GR_ANY, false, 90],
242
+ [GR_ANY, PR_ZWJ] => [GR_ANY, false, 90],
243
+ [GR_ANY, PR_SPACING_MARK] => [GR_ANY, false, 91],
244
+ [GR_ANY, PR_PREPEND] => [GR_PREPEND, true, 9990],
245
+ [GR_PREPEND, PR_ANY] => [GR_ANY, false, 92],
246
+ [GR_ANY, PR_EXTENDED_PICTOGRAPHIC] => [GR_EXTENDED_PICTOGRAPHIC, true, 9990],
247
+ [GR_EXTENDED_PICTOGRAPHIC, PR_EXTEND] => [GR_EXTENDED_PICTOGRAPHIC, false, 110],
248
+ [GR_EXTENDED_PICTOGRAPHIC, PR_ZWJ] => [GR_EXTENDED_PICTOGRAPHIC_ZWJ, false, 110],
249
+ [GR_EXTENDED_PICTOGRAPHIC_ZWJ, PR_EXTENDED_PICTOGRAPHIC] => [GR_EXTENDED_PICTOGRAPHIC, false, 110],
250
+ [GR_ANY, PR_REGIONAL_INDICATOR] => [GR_RI_ODD, true, 9990],
251
+ [GR_RI_ODD, PR_REGIONAL_INDICATOR] => [GR_RI_EVEN, false, 120],
252
+ [GR_RI_EVEN, PR_REGIONAL_INDICATOR] => [GR_RI_ODD, true, 120]
253
+ }.freeze
254
+
255
+ # transitionGraphemeState depends only on (state, property), so precompute it for every
256
+ # state (-1..10) and property (0..15). Entry: new_state << 1 | boundary.
257
+ GR_STATE_TABLE = begin
258
+ table = Array.new(12 * 16)
259
+ (-1..GR_RI_EVEN).each do |state|
260
+ (0..PR_EXTENDED_PICTOGRAPHIC).each do |prop|
261
+ new_state, boundary =
262
+ if (hit = GR_TRANSITIONS[[state, prop]])
263
+ [hit[0], hit[1]]
264
+ else
265
+ any_prop = GR_TRANSITIONS[[state, PR_ANY]]
266
+ any_state = GR_TRANSITIONS[[GR_ANY, prop]]
267
+ if any_prop && any_state
268
+ b = any_state[1]
269
+ b = any_prop[1] if any_prop[2] < any_state[2]
270
+ [any_state[0], b]
271
+ elsif any_prop
272
+ [any_prop[0], any_prop[1]]
273
+ elsif any_state
274
+ [any_state[0], any_state[1]]
275
+ else
276
+ [GR_ANY, true]
277
+ end
278
+ end
279
+ table[((state + 1) << 4) | prop] = (new_state << 1) | (boundary ? 1 : 0)
280
+ end
281
+ end
282
+ table.freeze
283
+ end
284
+
285
+ GRAPHEME_TABLE = Tables::GRAPHEME
286
+ GRAPHEME_COUNT = GRAPHEME_TABLE.size / 3
287
+ WIDE_TABLE = Tables::WIDE
288
+ WIDE_COUNT = WIDE_TABLE.size / 2
289
+ EMOJI_TABLE = Tables::EMOJI_PRESENTATION
290
+ EMOJI_COUNT = EMOJI_TABLE.size / 2
291
+
292
+ # Memoized per-code-point lookups (the binary searches dominate width measurement).
293
+ PROPERTY_CACHE = {}
294
+ EAW_WIDTH_CACHE = {}
295
+ EMOJI_WIDTH_CACHE = {}
296
+
297
+ # Go's unicode.IsSpace above Latin-1 (the White_Space property).
298
+ SPACE_ABOVE_LATIN1 = [0x1680, *0x2000..0x200A, 0x2028, 0x2029, 0x202F, 0x205F, 0x3000].freeze
299
+
300
+ NBSP = 0xA0
301
+ ESC = 0x1B
302
+ BEL = 0x07
303
+ CAN = 0x18
304
+ SUB = 0x1A
305
+ US = 0x1F
306
+ DEL = 0x7F
307
+ DCS = 0x90
308
+ SOS = 0x98
309
+ CSI = 0x9B
310
+ ST = 0x9C
311
+ OSC = 0x9D
312
+ PM = 0x9E
313
+ APC = 0x9F
314
+
315
+ RESET_STYLE = "\e[m"
316
+
317
+ # Bytes that can move the parser out of the ground state or start a UTF-8 sequence.
318
+ SPECIAL_BYTE = Regexp.new("[\\x1b\\x80-\\xff]".b, Regexp::NOENCODING)
319
+
320
+ extend self
321
+
322
+ # ansi.StringWidth: the number of terminal cells s occupies. ANSI escape sequences are
323
+ # ignored; text is measured per grapheme cluster with uniseg widths.
324
+ def string_width(s)
325
+ return 0 if s.empty?
326
+ return s.count(" -~") if s.ascii_only? && !s.include?("\e")
327
+
328
+ b = s.b
329
+ n = b.bytesize
330
+ pstate = GROUND
331
+ width = 0
332
+ i = 0
333
+ while i < n
334
+ if pstate == GROUND
335
+ # Fast path: in the ground state every byte below 0x80 other than ESC is printed (width 1)
336
+ # or executed (width 0) and leaves the parser in the ground state.
337
+ j = b.index(SPECIAL_BYTE, i) || n
338
+ if j > i
339
+ width += b.byteslice(i, j - i).count(" -~")
340
+ i = j
341
+ next
342
+ end
343
+ end
344
+ v = TABLE[(pstate << 8) | b.getbyte(i)]
345
+ state = v & 15
346
+ if state == UTF8
347
+ len, w, = cluster_at(b, i, -1)
348
+ width += w
349
+ i += len
350
+ pstate = GROUND
351
+ next
352
+ end
353
+ width += 1 if (v >> 4) == A_PRINT
354
+ pstate = state
355
+ i += 1
356
+ end
357
+ width
358
+ end
359
+
360
+ # ansi.Strip: removes ANSI escape sequences, keeping printable text and control characters.
361
+ def strip(s)
362
+ return s.dup if s.ascii_only? && !s.include?("\e")
363
+
364
+ b = s.b
365
+ buf = String.new(capacity: b.bytesize, encoding: Encoding::BINARY)
366
+ ri = 0
367
+ rw = 0
368
+ pstate = GROUND
369
+ b.each_byte do |c|
370
+ if pstate == UTF8
371
+ buf << c
372
+ ri += 1
373
+ next if ri < rw
374
+
375
+ pstate = GROUND
376
+ ri = 0
377
+ rw = 0
378
+ next
379
+ end
380
+
381
+ v = TABLE[(pstate << 8) | c]
382
+ state = v & 15
383
+ case v >> 4
384
+ when A_COLLECT
385
+ if state == UTF8
386
+ rw = utf8_byte_len(c)
387
+ buf << c
388
+ ri += 1
389
+ end
390
+ when A_PRINT, A_EXECUTE
391
+ buf << c
392
+ end
393
+ pstate = state
394
+ end
395
+ buf.force_encoding(Encoding::UTF_8)
396
+ end
397
+
398
+ # ansi.Truncate: cuts s to `length` cells, appending `tail` when it was cut. Escape sequences
399
+ # after the cut are kept.
400
+ def truncate(s, length, tail = "")
401
+ return s if string_width(s) <= length
402
+
403
+ tail_b = tail.b
404
+ length -= string_width(tail)
405
+ return +"" if length.negative?
406
+
407
+ b = s.b
408
+ n = b.bytesize
409
+ buf = String.new(capacity: n, encoding: Encoding::BINARY)
410
+ cur_width = 0
411
+ ignoring = false
412
+ pstate = GROUND
413
+ i = 0
414
+ while i < n
415
+ c = b.getbyte(i)
416
+ v = TABLE[(pstate << 8) | c]
417
+ state = v & 15
418
+ if state == UTF8
419
+ len, width, = cluster_at(b, i, -1)
420
+ start = i
421
+ i += len
422
+ next if ignoring
423
+
424
+ if cur_width + width > length
425
+ ignoring = true
426
+ buf << tail_b
427
+ next
428
+ end
429
+
430
+ cur_width += width
431
+ buf << b.byteslice(start, len)
432
+ pstate = GROUND
433
+ next
434
+ end
435
+
436
+ if (v >> 4) == A_PRINT
437
+ if cur_width >= length && !ignoring
438
+ ignoring = true
439
+ buf << tail_b
440
+ end
441
+ if ignoring
442
+ i += 1
443
+ next
444
+ end
445
+ cur_width += 1
446
+ end
447
+ buf << c
448
+ i += 1
449
+
450
+ pstate = state
451
+ if cur_width > length && !ignoring
452
+ ignoring = true
453
+ buf << tail_b
454
+ end
455
+ end
456
+ buf.force_encoding(Encoding::UTF_8)
457
+ end
458
+
459
+ # ansi.TruncateLeft: removes the first n cells of s, prepending `prefix` when anything was cut.
460
+ def truncate_left(s, n, prefix = "")
461
+ return s if n <= 0
462
+
463
+ prefix_b = prefix.b
464
+ b = s.b
465
+ size = b.bytesize
466
+ buf = String.new(capacity: size, encoding: Encoding::BINARY)
467
+ cur_width = 0
468
+ ignoring = true
469
+ pstate = GROUND
470
+ i = 0
471
+ while i < size
472
+ unless ignoring
473
+ buf << b.byteslice(i, size - i)
474
+ break
475
+ end
476
+
477
+ c = b.getbyte(i)
478
+ v = TABLE[(pstate << 8) | c]
479
+ state = v & 15
480
+ if state == UTF8
481
+ len, width, = cluster_at(b, i, -1)
482
+ start = i
483
+ i += len
484
+ cur_width += width
485
+ if cur_width > n && ignoring
486
+ ignoring = false
487
+ buf << prefix_b
488
+ end
489
+ next if ignoring
490
+
491
+ buf << b.byteslice(start, len) if cur_width > n
492
+ pstate = GROUND
493
+ next
494
+ end
495
+
496
+ if (v >> 4) == A_PRINT
497
+ cur_width += 1
498
+ if cur_width > n && ignoring
499
+ ignoring = false
500
+ buf << prefix_b
501
+ end
502
+ if ignoring
503
+ i += 1
504
+ next
505
+ end
506
+ end
507
+ buf << c
508
+ i += 1
509
+
510
+ pstate = state
511
+ if cur_width > n && ignoring
512
+ ignoring = false
513
+ buf << prefix_b
514
+ end
515
+ end
516
+ buf.force_encoding(Encoding::UTF_8)
517
+ end
518
+
519
+ # ansi.Cut: the cells in [left, right) of s, keeping escape sequences intact.
520
+ def cut(s, left, right)
521
+ return +"" if right <= left
522
+ return truncate(s, right, "") if left.zero?
523
+
524
+ truncate_left(truncate(s, right, ""), left, "")
525
+ end
526
+
527
+ # ansi.Hardwrap: wraps at `limit` cells, breaking anywhere.
528
+ def hardwrap(s, limit, preserve_space)
529
+ return s if limit < 1
530
+
531
+ b = s.b
532
+ n = b.bytesize
533
+ buf = String.new(capacity: n + (n / limit) + 1, encoding: Encoding::BINARY)
534
+ cur_width = 0
535
+ force_newline = false
536
+ pstate = GROUND
537
+ i = 0
538
+ while i < n
539
+ c = b.getbyte(i)
540
+ v = TABLE[(pstate << 8) | c]
541
+ state = v & 15
542
+ action = v >> 4
543
+ if state == UTF8
544
+ len, width, = cluster_at(b, i, -1)
545
+ start = i
546
+ i += len
547
+ if cur_width + width > limit
548
+ buf << 0x0A
549
+ cur_width = 0
550
+ end
551
+ if !preserve_space && cur_width.zero? && len <= 4
552
+ r, = decode_rune(b, start, start + len)
553
+ if r != RUNE_ERROR && space?(r)
554
+ pstate = GROUND
555
+ next
556
+ end
557
+ end
558
+ buf << b.byteslice(start, len)
559
+ cur_width += width
560
+ pstate = GROUND
561
+ next
562
+ end
563
+
564
+ if action == A_PRINT || action == A_EXECUTE
565
+ if c == 0x0A
566
+ buf << 0x0A
567
+ cur_width = 0
568
+ force_newline = false
569
+ else
570
+ if cur_width + 1 > limit
571
+ buf << 0x0A
572
+ cur_width = 0
573
+ force_newline = true
574
+ end
575
+ skip = false
576
+ if cur_width.zero?
577
+ if !preserve_space && force_newline && space?(c)
578
+ skip = true
579
+ else
580
+ force_newline = false
581
+ end
582
+ end
583
+ unless skip
584
+ buf << c
585
+ cur_width += 1 if action == A_PRINT
586
+ end
587
+ end
588
+ else
589
+ buf << c
590
+ end
591
+
592
+ pstate = state
593
+ i += 1
594
+ end
595
+ buf.force_encoding(Encoding::UTF_8)
596
+ end
597
+
598
+ # ansi.Wordwrap: wraps at `limit` cells on word boundaries only (long words overflow).
599
+ # A hyphen and every rune in `breakpoints` are breakpoints.
600
+ def wordwrap(s, limit, breakpoints = "")
601
+ return s if limit < 1
602
+
603
+ bp_runes = breakpoint_runes(breakpoints)
604
+ bp_bytes = bp_runes.map { |r| [r].pack("U").b }
605
+ b = s.b
606
+ n = b.bytesize
607
+ buf = String.new(capacity: n + 16, encoding: Encoding::BINARY)
608
+ word = String.new(encoding: Encoding::BINARY)
609
+ space = String.new(encoding: Encoding::BINARY)
610
+ cur_width = 0
611
+ word_len = 0
612
+ pstate = GROUND
613
+
614
+ add_space = lambda do
615
+ cur_width += space.bytesize
616
+ buf << space
617
+ space.clear
618
+ end
619
+ add_word = lambda do
620
+ next if word.empty?
621
+
622
+ add_space.call
623
+ cur_width += word_len
624
+ buf << word
625
+ word.clear
626
+ word_len = 0
627
+ end
628
+ add_newline = lambda do
629
+ buf << 0x0A
630
+ cur_width = 0
631
+ space.clear
632
+ end
633
+
634
+ i = 0
635
+ while i < n
636
+ c = b.getbyte(i)
637
+ v = TABLE[(pstate << 8) | c]
638
+ state = v & 15
639
+ action = v >> 4
640
+ if state == UTF8
641
+ len, width, = cluster_at(b, i, -1)
642
+ cluster = b.byteslice(i, len)
643
+ i += len
644
+ r, = decode_rune(cluster, 0, len)
645
+ if r != RUNE_ERROR && space?(r) && r != NBSP
646
+ add_word.call
647
+ append_rune(space, r)
648
+ elsif bp_bytes.any? { |bp| cluster.include?(bp) }
649
+ add_space.call
650
+ add_word.call
651
+ buf << cluster
652
+ cur_width += 1
653
+ else
654
+ word << cluster
655
+ word_len += width
656
+ add_newline.call if cur_width + space.bytesize + word_len > limit && word_len < limit
657
+ end
658
+ pstate = GROUND
659
+ next
660
+ end
661
+
662
+ if action == A_PRINT || action == A_EXECUTE
663
+ if c == 0x0A
664
+ if word_len.zero?
665
+ if cur_width + space.bytesize > limit
666
+ cur_width = 0
667
+ else
668
+ buf << space
669
+ end
670
+ space.clear
671
+ end
672
+ add_word.call
673
+ add_newline.call
674
+ elsif space?(c)
675
+ add_word.call
676
+ space << c
677
+ elsif c == 0x2D || bp_runes.include?(c)
678
+ add_space.call
679
+ add_word.call
680
+ buf << c
681
+ cur_width += 1
682
+ else
683
+ word << c
684
+ word_len += 1
685
+ add_newline.call if cur_width + space.bytesize + word_len > limit && word_len < limit
686
+ end
687
+ else
688
+ word << c
689
+ end
690
+
691
+ pstate = state
692
+ i += 1
693
+ end
694
+
695
+ add_word.call
696
+ buf.force_encoding(Encoding::UTF_8)
697
+ end
698
+
699
+ # ansi.Wrap: wraps at `limit` cells on word boundaries, breaking words longer than the limit.
700
+ # A hyphen and every rune in `breakpoints` are breakpoints. Used by lipgloss's table resizing.
701
+ def wrap(s, limit, breakpoints = "")
702
+ return s if limit < 1
703
+
704
+ bp_runes = breakpoint_runes(breakpoints)
705
+ bp_bytes = bp_runes.map { |r| [r].pack("U").b }
706
+ b = s.b
707
+ n = b.bytesize
708
+ buf = String.new(capacity: n + 16, encoding: Encoding::BINARY)
709
+ word = String.new(encoding: Encoding::BINARY)
710
+ space = String.new(encoding: Encoding::BINARY)
711
+ cur_width = 0
712
+ word_len = 0
713
+ pstate = GROUND
714
+
715
+ add_space = lambda do
716
+ cur_width += space.bytesize
717
+ buf << space
718
+ space.clear
719
+ end
720
+ add_word = lambda do
721
+ next if word.empty?
722
+
723
+ add_space.call
724
+ cur_width += word_len
725
+ buf << word
726
+ word.clear
727
+ word_len = 0
728
+ end
729
+ add_newline = lambda do
730
+ buf << 0x0A
731
+ cur_width = 0
732
+ space.clear
733
+ end
734
+
735
+ i = 0
736
+ while i < n
737
+ c = b.getbyte(i)
738
+ v = TABLE[(pstate << 8) | c]
739
+ state = v & 15
740
+ action = v >> 4
741
+ if state == UTF8
742
+ len, width, = cluster_at(b, i, -1)
743
+ cluster = b.byteslice(i, len)
744
+ i += len
745
+ r, = decode_rune(cluster, 0, len)
746
+ if r != RUNE_ERROR && space?(r) && r != NBSP
747
+ add_word.call
748
+ append_rune(space, r)
749
+ elsif bp_bytes.any? { |bp| cluster.include?(bp) }
750
+ add_space.call
751
+ if cur_width + word_len + width > limit
752
+ word << cluster
753
+ word_len += width
754
+ else
755
+ add_word.call
756
+ buf << cluster
757
+ cur_width += width
758
+ end
759
+ else
760
+ add_word.call if word_len + width > limit
761
+ word << cluster
762
+ word_len += width
763
+ add_newline.call if cur_width + word_len + space.bytesize > limit
764
+ end
765
+ pstate = GROUND
766
+ next
767
+ end
768
+
769
+ if action == A_PRINT || action == A_EXECUTE
770
+ if c == 0x0A
771
+ if word_len.zero?
772
+ if cur_width + space.bytesize > limit
773
+ cur_width = 0
774
+ else
775
+ buf << space
776
+ end
777
+ space.clear
778
+ end
779
+ add_word.call
780
+ add_newline.call
781
+ elsif space?(c)
782
+ add_word.call
783
+ append_rune(space, c)
784
+ elsif c == 0x2D || bp_runes.include?(c)
785
+ add_space.call
786
+ if cur_width + word_len >= limit
787
+ append_rune(word, c)
788
+ word_len += 1
789
+ else
790
+ add_word.call
791
+ append_rune(buf, c)
792
+ cur_width += 1
793
+ end
794
+ else
795
+ add_newline.call if cur_width == limit
796
+ append_rune(word, c)
797
+ word_len += 1
798
+ add_word.call if word_len == limit
799
+ add_newline.call if cur_width + word_len + space.bytesize > limit
800
+ end
801
+ else
802
+ word << c
803
+ end
804
+
805
+ pstate = state
806
+ i += 1
807
+ end
808
+
809
+ if word_len.zero?
810
+ if cur_width + space.bytesize > limit
811
+ cur_width = 0
812
+ else
813
+ buf << space
814
+ end
815
+ space.clear
816
+ end
817
+ add_word.call
818
+ buf.force_encoding(Encoding::UTF_8)
819
+ end
820
+
821
+ # cellbuf.Wrap: like ansi.Wrap, but tracks the active SGR style and OSC 8 hyperlink and
822
+ # closes/reopens them around every inserted line break. Used by lipgloss Style#Render.
823
+ def cellbuf_wrap(s, limit, breakpoints = "")
824
+ return +"" if s.empty?
825
+ return s if limit < 1
826
+
827
+ bp_runes = breakpoint_runes(breakpoints)
828
+ p = SeqParser.new
829
+ b = s.b
830
+ n = b.bytesize
831
+ buf = String.new(capacity: n + 16, encoding: Encoding::BINARY)
832
+ word = String.new(encoding: Encoding::BINARY)
833
+ space = String.new(encoding: Encoding::BINARY)
834
+ style = CellStyle.new
835
+ cur_style = CellStyle.new
836
+ link = [+"", +""] # [url, params]
837
+ cur_link = ["", ""]
838
+ cur_width = 0
839
+ word_len = 0
840
+
841
+ add_space = lambda do
842
+ cur_width += space.bytesize
843
+ buf << space
844
+ space.clear
845
+ end
846
+ add_word = lambda do
847
+ next if word.empty?
848
+
849
+ cur_link = link.dup
850
+ cur_style = style.dup
851
+ add_space.call
852
+ cur_width += word_len
853
+ buf << word
854
+ word.clear
855
+ word_len = 0
856
+ end
857
+ add_newline = lambda do
858
+ link_set = !(cur_link[0].empty? && cur_link[1].empty?)
859
+ buf << RESET_STYLE unless cur_style.empty?
860
+ buf << "\e]8;;\a" if link_set
861
+ buf << 0x0A
862
+ buf << "\e]8;" << cur_link[1].b << ";" << cur_link[0].b << "\a" if link_set
863
+ buf << cur_style.sequence unless cur_style.empty?
864
+ cur_width = 0
865
+ space.clear
866
+ end
867
+
868
+ i = 0
869
+ state = 0
870
+ while i < n
871
+ len, width, state = decode_sequence(b, i, state, p)
872
+ seq = b.byteslice(i, len)
873
+ if width.zero?
874
+ if seq == "\t"
875
+ add_word.call
876
+ space << seq
877
+ elsif seq == "\n"
878
+ if word_len.zero?
879
+ if cur_width + space.bytesize > limit
880
+ cur_width = 0
881
+ else
882
+ buf << space
883
+ end
884
+ space.clear
885
+ end
886
+ add_word.call
887
+ add_newline.call
888
+ else
889
+ f = seq.getbyte(0)
890
+ s1 = seq.getbyte(1)
891
+ if (f == CSI || (f == ESC && s1 == 0x5B)) && p.cmd == 0x6D
892
+ read_style(p.params_slice, style)
893
+ elsif (f == OSC || (f == ESC && s1 == 0x5D)) && p.cmd == 8
894
+ read_link(p.data, link)
895
+ end
896
+ word << seq
897
+ end
898
+ else
899
+ handled = false
900
+ if len == 1
901
+ r = seq.getbyte(0)
902
+ r = RUNE_ERROR if r >= 0x80 # a lone invalid UTF-8 byte decodes to RuneError
903
+ if space?(r)
904
+ add_word.call
905
+ append_rune(space, r)
906
+ handled = true
907
+ elsif r == 0x2D || bp_runes.include?(r)
908
+ add_space.call
909
+ if cur_width + word_len + width <= limit
910
+ add_word.call
911
+ buf << seq
912
+ cur_width += width
913
+ handled = true
914
+ end
915
+ end
916
+ end
917
+
918
+ unless handled
919
+ add_word.call if word_len + width > limit
920
+ word << seq
921
+ word_len += width
922
+ add_newline.call if cur_width + word_len + space.bytesize > limit
923
+ end
924
+ end
925
+ i += len
926
+ end
927
+
928
+ if word_len.zero?
929
+ if cur_width + space.bytesize > limit
930
+ cur_width = 0
931
+ else
932
+ buf << space
933
+ end
934
+ space.clear
935
+ end
936
+ add_word.call
937
+
938
+ buf << "\e]8;;\a" unless cur_link[0].empty? && cur_link[1].empty?
939
+ buf << RESET_STYLE unless cur_style.empty?
940
+ buf.force_encoding(Encoding::UTF_8)
941
+ end
942
+
943
+ # uniseg.FirstGraphemeClusterInString with a fresh state: [cluster, width].
944
+ def first_grapheme_cluster(s)
945
+ cluster, _rest, width, = first_grapheme_cluster_in_string(s, -1)
946
+ [cluster, width]
947
+ end
948
+
949
+ # uniseg.FirstGraphemeClusterInString(str, state) -> [cluster, rest, width, new_state].
950
+ # Pass -1 as the state for the first call and the returned state for the following ones.
951
+ def first_grapheme_cluster_in_string(s, state = -1)
952
+ return ["", "", 0, 0] if s.empty?
953
+
954
+ b = s.b
955
+ len, width, new_state = cluster_at(b, 0, state)
956
+ cluster = b.byteslice(0, len).force_encoding(Encoding::UTF_8)
957
+ rest = b.byteslice(len, b.bytesize - len).force_encoding(Encoding::UTF_8)
958
+ [cluster, rest, width, new_state]
959
+ end
960
+
961
+ # --- internals ----------------------------------------------------------------------------
962
+
963
+ # uniseg.FirstGraphemeCluster on the bytes of `b` (a binary string) starting at `i`.
964
+ # Returns [byte length, width, new state].
965
+ def cluster_at(b, i, state)
966
+ n = b.bytesize
967
+ r, length = decode_rune(b, i, n)
968
+ if n - i <= length
969
+ prop = state.negative? ? grapheme_property(r) : state >> 4
970
+ return [n - i, rune_width(r, prop), GR_ANY | (prop << 4)]
971
+ end
972
+
973
+ if state.negative?
974
+ first_prop = grapheme_property(r)
975
+ state = GR_STATE_TABLE[first_prop] >> 1 # row for state -1
976
+ else
977
+ first_prop = state >> 4
978
+ end
979
+ width = rune_width(r, first_prop)
980
+
981
+ loop do
982
+ r, l = decode_rune(b, i + length, n)
983
+ prop = grapheme_property(r)
984
+ t = GR_STATE_TABLE[(((state & 0xF) + 1) << 4) | prop]
985
+ state = t >> 1
986
+ return [length, width, state | (prop << 4)] if t.odd?
987
+
988
+ if first_prop == PR_EXTENDED_PICTOGRAPHIC
989
+ if r == VS15
990
+ width = 1
991
+ elsif r == VS16
992
+ width = 2
993
+ end
994
+ elsif first_prop != PR_REGIONAL_INDICATOR && first_prop != PR_L
995
+ width += rune_width(r, prop)
996
+ end
997
+
998
+ length += l
999
+ return [n - i, width, GR_ANY | (prop << 4)] if n - i <= length
1000
+ end
1001
+ end
1002
+
1003
+ # utf8.DecodeRune on b[i...n]: [rune, size]; invalid input yields [RUNE_ERROR, 1].
1004
+ def decode_rune(b, i, n)
1005
+ c0 = b.getbyte(i)
1006
+ return [c0, 1] if c0 < 0x80
1007
+ return [RUNE_ERROR, 1] if c0 < 0xC2 || c0 > 0xF4
1008
+
1009
+ avail = n - i
1010
+ return [RUNE_ERROR, 1] if avail < 2
1011
+
1012
+ c1 = b.getbyte(i + 1)
1013
+ lo = 0x80
1014
+ hi = 0xBF
1015
+ if c0 == 0xE0
1016
+ lo = 0xA0
1017
+ elsif c0 == 0xED
1018
+ hi = 0x9F
1019
+ elsif c0 == 0xF0
1020
+ lo = 0x90
1021
+ elsif c0 == 0xF4
1022
+ hi = 0x8F
1023
+ end
1024
+ return [RUNE_ERROR, 1] if c1 < lo || c1 > hi
1025
+ return [((c0 & 0x1F) << 6) | (c1 & 0x3F), 2] if c0 < 0xE0
1026
+ return [RUNE_ERROR, 1] if avail < 3
1027
+
1028
+ c2 = b.getbyte(i + 2)
1029
+ return [RUNE_ERROR, 1] if c2 < 0x80 || c2 > 0xBF
1030
+ return [((c0 & 0x0F) << 12) | ((c1 & 0x3F) << 6) | (c2 & 0x3F), 3] if c0 < 0xF0
1031
+ return [RUNE_ERROR, 1] if avail < 4
1032
+
1033
+ c3 = b.getbyte(i + 3)
1034
+ return [RUNE_ERROR, 1] if c3 < 0x80 || c3 > 0xBF
1035
+
1036
+ [((c0 & 0x07) << 18) | ((c1 & 0x3F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F), 4]
1037
+ end
1038
+
1039
+ def grapheme_property(r)
1040
+ return PR_ANY if r >= 0x20 && r <= 0x7E
1041
+ return PR_LF if r == 0x0A
1042
+ return PR_CR if r == 0x0D
1043
+ return PR_CONTROL if r <= 0x1F || r == 0x7F
1044
+
1045
+ PROPERTY_CACHE[r] ||= search_grapheme_property(r)
1046
+ end
1047
+
1048
+ def search_grapheme_property(r)
1049
+ lo = 0
1050
+ hi = GRAPHEME_COUNT
1051
+ t = GRAPHEME_TABLE
1052
+ while hi > lo
1053
+ mid = (lo + hi) >> 1
1054
+ base = mid * 3
1055
+ if r < t[base]
1056
+ hi = mid
1057
+ elsif r > t[base + 1]
1058
+ lo = mid + 1
1059
+ else
1060
+ return t[base + 2]
1061
+ end
1062
+ end
1063
+ 0
1064
+ end
1065
+
1066
+ def in_ranges?(t, count, r)
1067
+ lo = 0
1068
+ hi = count
1069
+ while hi > lo
1070
+ mid = (lo + hi) >> 1
1071
+ base = mid << 1
1072
+ if r < t[base]
1073
+ hi = mid
1074
+ elsif r > t[base + 1]
1075
+ lo = mid + 1
1076
+ else
1077
+ return true
1078
+ end
1079
+ end
1080
+ false
1081
+ end
1082
+
1083
+ # uniseg runeWidth (EastAsianAmbiguousWidth = 1).
1084
+ def rune_width(r, prop)
1085
+ case prop
1086
+ when PR_CONTROL, PR_CR, PR_LF, PR_EXTEND, PR_ZWJ
1087
+ return 0
1088
+ when PR_REGIONAL_INDICATOR
1089
+ return 2
1090
+ when PR_EXTENDED_PICTOGRAPHIC
1091
+ return EMOJI_WIDTH_CACHE[r] ||= in_ranges?(EMOJI_TABLE, EMOJI_COUNT, r) ? 2 : 1
1092
+ end
1093
+ return 1 if r < 0x1100 # nothing below U+1100 is East Asian Wide or Fullwidth
1094
+
1095
+ EAW_WIDTH_CACHE[r] ||=
1096
+ if r == 0x2E3A then 3
1097
+ elsif r == 0x2E3B then 4
1098
+ else in_ranges?(WIDE_TABLE, WIDE_COUNT, r) ? 2 : 1
1099
+ end
1100
+ end
1101
+
1102
+ # Go's unicode.IsSpace.
1103
+ def space?(r)
1104
+ if r <= 0xFF
1105
+ r == 0x20 || (r >= 0x09 && r <= 0x0D) || r == 0x85 || r == 0xA0
1106
+ else
1107
+ SPACE_ABOVE_LATIN1.include?(r)
1108
+ end
1109
+ end
1110
+
1111
+ def utf8_byte_len(c)
1112
+ if c <= 0x7F then 1
1113
+ elsif c >= 0xC0 && c <= 0xDF then 2
1114
+ elsif c >= 0xE0 && c <= 0xEF then 3
1115
+ elsif c >= 0xF0 && c <= 0xF7 then 4
1116
+ else -1
1117
+ end
1118
+ end
1119
+
1120
+ # bytes.Buffer#WriteRune into a binary buffer.
1121
+ def append_rune(buf, r)
1122
+ if r < 0x80
1123
+ buf << r
1124
+ else
1125
+ buf << [r].pack("U").b
1126
+ end
1127
+ end
1128
+
1129
+ # The runes of a Go string (invalid bytes become U+FFFD).
1130
+ def breakpoint_runes(breakpoints)
1131
+ return [] if breakpoints.nil? || breakpoints.empty?
1132
+
1133
+ breakpoints.b.force_encoding(Encoding::UTF_8).scrub("�").codepoints
1134
+ end
1135
+
1136
+ # Parser state collected by ansi.DecodeSequence (the parts cellbuf.Wrap reads).
1137
+ class SeqParser
1138
+ MAX_PARAMS = 32
1139
+ MAX_DATA = 4 * 1024 * 1024
1140
+ MISSING_PARAM = 0x7FFFFFFF
1141
+ HAS_MORE_FLAG = -0x80000000
1142
+ MISSING_COMMAND = MISSING_PARAM
1143
+
1144
+ attr_accessor :cmd, :params, :params_len, :data
1145
+
1146
+ def initialize
1147
+ @params = Array.new(MAX_PARAMS, 0)
1148
+ @params_len = 0
1149
+ @data = String.new(encoding: Encoding::BINARY)
1150
+ @cmd = 0
1151
+ end
1152
+
1153
+ def params_slice
1154
+ @params[0, @params_len]
1155
+ end
1156
+ end
1157
+
1158
+ # ansi.DecodeSequence on b starting at i: [byte length, width, new state].
1159
+ def decode_sequence(b, i, state, p)
1160
+ n = b.bytesize
1161
+ j = i
1162
+ while j < n
1163
+ c = b.getbyte(j)
1164
+ case state
1165
+ when 0 # NormalState
1166
+ case c
1167
+ when ESC
1168
+ p.params[0] = SeqParser::MISSING_PARAM
1169
+ p.cmd = 0
1170
+ p.params_len = 0
1171
+ p.data.clear
1172
+ state = 4
1173
+ j += 1
1174
+ next
1175
+ when CSI, DCS
1176
+ p.params[0] = SeqParser::MISSING_PARAM
1177
+ p.cmd = 0
1178
+ p.params_len = 0
1179
+ p.data.clear
1180
+ state = 1
1181
+ j += 1
1182
+ next
1183
+ when OSC, APC, SOS, PM
1184
+ p.cmd = SeqParser::MISSING_COMMAND
1185
+ p.data.clear
1186
+ state = 5
1187
+ j += 1
1188
+ next
1189
+ end
1190
+
1191
+ p.data.clear
1192
+ p.params_len = 0
1193
+ p.cmd = 0
1194
+ return [1, 1, 0] if c > US && c < DEL
1195
+ return [1, 0, 0] if c <= US || c == DEL || c < 0xC0
1196
+
1197
+ len, width, = cluster_at(b, i, -1)
1198
+ return [len, width, 0]
1199
+ when 1, 2, 3 # Prefix, Params, Intermed (with Go's fallthrough)
1200
+ if state == 1
1201
+ if c >= 0x3C && c <= 0x3F
1202
+ p.cmd = (p.cmd & ~(0xFF << 8)) | (c << 8)
1203
+ j += 1
1204
+ next
1205
+ end
1206
+ state = 2
1207
+ end
1208
+ if state == 2
1209
+ if c >= 0x30 && c <= 0x39
1210
+ pl = p.params_len
1211
+ cur = p.params[pl]
1212
+ cur = 0 if cur.nil? || cur == SeqParser::MISSING_PARAM
1213
+ p.params[pl] = (cur * 10) + (c - 0x30)
1214
+ j += 1
1215
+ next
1216
+ end
1217
+ if c == 0x3A
1218
+ p.params[p.params_len] = (p.params[p.params_len] || SeqParser::MISSING_PARAM) | SeqParser::HAS_MORE_FLAG
1219
+ end
1220
+ if c == 0x3B || c == 0x3A
1221
+ p.params_len += 1
1222
+ p.params[p.params_len] = SeqParser::MISSING_PARAM if p.params_len < SeqParser::MAX_PARAMS
1223
+ j += 1
1224
+ next
1225
+ end
1226
+ state = 3
1227
+ end
1228
+ if c >= 0x20 && c <= 0x2F
1229
+ p.cmd = (p.cmd & ~(0xFF << 16)) | (c << 16)
1230
+ j += 1
1231
+ next
1232
+ end
1233
+
1234
+ pl = p.params_len
1235
+ if (pl.positive? && pl < SeqParser::MAX_PARAMS - 1) ||
1236
+ (pl.zero? && p.params[0] != SeqParser::MISSING_PARAM)
1237
+ p.params_len += 1
1238
+ end
1239
+
1240
+ if c >= 0x40 && c <= 0x7E
1241
+ p.cmd = (p.cmd & ~0xFF) | c
1242
+ f = b.getbyte(i)
1243
+ if f == DCS || (f == ESC && b.getbyte(i + 1) == 0x50)
1244
+ p.data.clear
1245
+ state = 5
1246
+ j += 1
1247
+ next
1248
+ end
1249
+ return [j + 1 - i, 0, 0]
1250
+ end
1251
+ return [j - i, 0, 0]
1252
+ when 4 # EscapeState
1253
+ case c
1254
+ when 0x5B, 0x50
1255
+ p.params[0] = SeqParser::MISSING_PARAM
1256
+ p.params_len = 0
1257
+ p.cmd = 0
1258
+ state = 1
1259
+ j += 1
1260
+ next
1261
+ when 0x5D, 0x58, 0x5E, 0x5F
1262
+ p.cmd = SeqParser::MISSING_COMMAND
1263
+ p.data.clear
1264
+ state = 5
1265
+ j += 1
1266
+ next
1267
+ end
1268
+
1269
+ if c >= 0x20 && c <= 0x2F
1270
+ p.cmd = (p.cmd & ~(0xFF << 16)) | (c << 16)
1271
+ j += 1
1272
+ next
1273
+ elsif c >= 0x30 && c <= 0x7E
1274
+ p.cmd = (p.cmd & ~0xFF) | c
1275
+ return [j + 1 - i, 0, 0]
1276
+ end
1277
+ return [j - i, 0, 0]
1278
+ when 5 # StringState
1279
+ osc = osc_prefix?(b, i)
1280
+ case c
1281
+ when BEL
1282
+ if osc
1283
+ parse_osc_cmd(p)
1284
+ return [j + 1 - i, 0, 0]
1285
+ end
1286
+ when CAN, SUB
1287
+ parse_osc_cmd(p) if osc
1288
+ return [j - i, 0, 0]
1289
+ when ST
1290
+ parse_osc_cmd(p) if osc
1291
+ return [j + 1 - i, 0, 0]
1292
+ when ESC
1293
+ if b.getbyte(j + 1) == 0x5C
1294
+ parse_osc_cmd(p) if osc
1295
+ return [j + 2 - i, 0, 0]
1296
+ end
1297
+ return [j - i, 0, 0]
1298
+ end
1299
+
1300
+ if p.data.bytesize < SeqParser::MAX_DATA
1301
+ p.data << c
1302
+ parse_osc_cmd(p) if c == 0x3B && osc
1303
+ end
1304
+ end
1305
+ j += 1
1306
+ end
1307
+ [n - i, 0, state]
1308
+ end
1309
+
1310
+ def osc_prefix?(b, i)
1311
+ f = b.getbyte(i)
1312
+ f == OSC || (f == ESC && b.getbyte(i + 1) == 0x5D)
1313
+ end
1314
+
1315
+ def parse_osc_cmd(p)
1316
+ return unless p.cmd == SeqParser::MISSING_COMMAND
1317
+
1318
+ p.data.each_byte do |d|
1319
+ break if d < 0x30 || d > 0x39
1320
+
1321
+ p.cmd = 0 if p.cmd == SeqParser::MISSING_COMMAND
1322
+ p.cmd = (p.cmd * 10) + (d - 0x30)
1323
+ end
1324
+ end
1325
+
1326
+ # cellbuf.Style: SGR state. Colors are [:basic, n], [:ext, n], [:rgb, r, g, b] (any
1327
+ # color.Color, already reduced to 8-bit channels by ansi's shift) or nil.
1328
+ class CellStyle
1329
+ BOLD = 1
1330
+ FAINT = 2
1331
+ ITALIC = 4
1332
+ SLOW_BLINK = 8
1333
+ RAPID_BLINK = 16
1334
+ REVERSE = 32
1335
+ CONCEAL = 64
1336
+ STRIKETHROUGH = 128
1337
+ ATTR_CODES = [[BOLD, "1"], [FAINT, "2"], [ITALIC, "3"], [SLOW_BLINK, "5"], [RAPID_BLINK, "6"],
1338
+ [REVERSE, "7"], [CONCEAL, "8"], [STRIKETHROUGH, "9"]].freeze
1339
+ UNDERLINE_CODES = [nil, "4", "4:2", "4:3", "4:4", "4:5"].freeze
1340
+
1341
+ attr_accessor :fg, :bg, :ul, :attrs, :ul_style
1342
+
1343
+ def initialize
1344
+ reset
1345
+ end
1346
+
1347
+ def reset
1348
+ @fg = nil
1349
+ @bg = nil
1350
+ @ul = nil
1351
+ @attrs = 0
1352
+ @ul_style = 0
1353
+ end
1354
+
1355
+ def set_attr(mask, on)
1356
+ @attrs = on ? (@attrs | mask) : (@attrs & ~mask)
1357
+ end
1358
+
1359
+ def empty?
1360
+ @fg.nil? && @bg.nil? && @ul.nil? && @attrs.zero? && @ul_style.zero?
1361
+ end
1362
+
1363
+ def sequence
1364
+ return RESET_STYLE if empty?
1365
+
1366
+ parts = []
1367
+ ATTR_CODES.each { |mask, code| parts << code if @attrs & mask != 0 }
1368
+ parts << UNDERLINE_CODES[@ul_style] if @ul_style != 0
1369
+ parts << color_string(@fg, 30, 90, "38") if @fg
1370
+ parts << color_string(@bg, 40, 100, "48") if @bg
1371
+ parts << underline_color_string(@ul) if @ul
1372
+ "\e[#{parts.join(';')}m"
1373
+ end
1374
+
1375
+ private
1376
+
1377
+ def color_string(color, base, bright_base, ext)
1378
+ case color[0]
1379
+ when :basic
1380
+ n = color[1]
1381
+ n < 8 ? (base + n).to_s : (bright_base + n - 8).to_s
1382
+ when :ext then "#{ext};5;#{color[1]}"
1383
+ else "#{ext};2;#{color[1]};#{color[2]};#{color[3]}"
1384
+ end
1385
+ end
1386
+
1387
+ def underline_color_string(color)
1388
+ case color[0]
1389
+ when :basic, :ext then "58;5;#{color[1]}"
1390
+ else "58;2;#{color[1]};#{color[2]};#{color[3]}"
1391
+ end
1392
+ end
1393
+ end
1394
+
1395
+ PARAM_MASK = 0x7FFFFFFF
1396
+
1397
+ def param_value(raw, default)
1398
+ return default if raw.nil?
1399
+
1400
+ v = raw & PARAM_MASK
1401
+ v == SeqParser::MISSING_PARAM ? default : v
1402
+ end
1403
+
1404
+ def param_more?(raw)
1405
+ !raw.nil? && (raw & SeqParser::HAS_MORE_FLAG) != 0
1406
+ end
1407
+
1408
+ # cellbuf.ReadStyle.
1409
+ def read_style(params, pen)
1410
+ if params.empty?
1411
+ pen.reset
1412
+ return
1413
+ end
1414
+
1415
+ i = 0
1416
+ while i < params.size
1417
+ param = param_value(params[i], 0)
1418
+ has_more = param_more?(params[i])
1419
+ case param
1420
+ when 0 then pen.reset
1421
+ when 1 then pen.set_attr(CellStyle::BOLD, true)
1422
+ when 2 then pen.set_attr(CellStyle::FAINT, true)
1423
+ when 3 then pen.set_attr(CellStyle::ITALIC, true)
1424
+ when 4
1425
+ ok = i + 1 < params.size
1426
+ next_param = ok ? param_value(params[i + 1], 0) : 0
1427
+ if has_more && ok
1428
+ if next_param.between?(0, 5)
1429
+ i += 1
1430
+ pen.ul_style = next_param
1431
+ end
1432
+ else
1433
+ pen.ul_style = 1
1434
+ end
1435
+ when 5 then pen.set_attr(CellStyle::SLOW_BLINK, true)
1436
+ when 6 then pen.set_attr(CellStyle::RAPID_BLINK, true)
1437
+ when 7 then pen.set_attr(CellStyle::REVERSE, true)
1438
+ when 8 then pen.set_attr(CellStyle::CONCEAL, true)
1439
+ when 9 then pen.set_attr(CellStyle::STRIKETHROUGH, true)
1440
+ when 22
1441
+ pen.set_attr(CellStyle::BOLD, false)
1442
+ pen.set_attr(CellStyle::FAINT, false)
1443
+ when 23 then pen.set_attr(CellStyle::ITALIC, false)
1444
+ when 24 then pen.ul_style = 0
1445
+ when 25
1446
+ pen.set_attr(CellStyle::SLOW_BLINK, false)
1447
+ pen.set_attr(CellStyle::RAPID_BLINK, false)
1448
+ when 27 then pen.set_attr(CellStyle::REVERSE, false)
1449
+ when 28 then pen.set_attr(CellStyle::CONCEAL, false)
1450
+ when 29 then pen.set_attr(CellStyle::STRIKETHROUGH, false)
1451
+ when 30..37 then pen.fg = [:basic, param - 30]
1452
+ when 38, 48, 58
1453
+ n, color = read_style_color(params[i..])
1454
+ if n.positive?
1455
+ case param
1456
+ when 38 then pen.fg = color
1457
+ when 48 then pen.bg = color
1458
+ else pen.ul = color
1459
+ end
1460
+ i += n - 1
1461
+ end
1462
+ when 39 then pen.fg = nil
1463
+ when 40..47 then pen.bg = [:basic, param - 40]
1464
+ when 49 then pen.bg = nil
1465
+ when 59 then pen.ul = nil
1466
+ when 90..97 then pen.fg = [:basic, param - 90 + 8]
1467
+ when 100..107 then pen.bg = [:basic, param - 100 + 8]
1468
+ end
1469
+ i += 1
1470
+ end
1471
+ end
1472
+
1473
+ # ansi.ReadStyleColor: [params consumed, color or nil].
1474
+ def read_style_color(params)
1475
+ return [0, nil] if params.size < 2
1476
+
1477
+ s_more = param_more?(params[0])
1478
+ p_more = param_more?(params[1])
1479
+ color_type = param_value(params[1], 0)
1480
+ n = 2
1481
+ more = ->(k) { param_more?(params[k]) }
1482
+ val = ->(k) { param_value(params[k], 0) }
1483
+
1484
+ channels = lambda do
1485
+ if s_more && p_more && params.size > 8 && more.(2) && more.(3) && more.(4) && more.(5) && more.(6) && more.(7)
1486
+ n += 7
1487
+ [val.(3), val.(4), val.(5), val.(6)]
1488
+ elsif s_more && p_more && params.size > 7 && more.(2) && more.(3) && more.(4) && more.(5) && more.(6)
1489
+ n += 6
1490
+ [val.(3), val.(4), val.(5), val.(6)]
1491
+ elsif s_more && p_more && params.size > 6 && more.(2) && more.(3) && more.(4) && more.(5)
1492
+ n += 5
1493
+ [val.(3), val.(4), val.(5), val.(6)]
1494
+ elsif s_more && p_more && params.size > 5 && more.(2) && more.(3) && more.(4) && !more.(5)
1495
+ n += 4
1496
+ [val.(3), val.(4), val.(5), -1]
1497
+ elsif (s_more && p_more && val.(1) == 2 && more.(2) && more.(3) && !more.(4)) ||
1498
+ (!s_more && !p_more && val.(1) == 2 && !more.(2) && !more.(3) && !more.(4))
1499
+ n += 3
1500
+ [val.(2), val.(3), val.(4), -1]
1501
+ else
1502
+ [-1, -1, -1, -1]
1503
+ end
1504
+ end
1505
+
1506
+ case color_type
1507
+ when 0 then [2, nil]
1508
+ when 1 then [2, [:rgb, 0, 0, 0]] # color.Transparent
1509
+ when 2, 6
1510
+ return [0, nil] if params.size < (color_type == 2 ? 5 : 6)
1511
+
1512
+ r, g, b, a = channels.call
1513
+ return [0, nil] if r == -1 || g == -1 || b == -1 || (color_type == 6 && a == -1)
1514
+
1515
+ [n, [:rgb, r & 0xFF, g & 0xFF, b & 0xFF]]
1516
+ when 3, 4
1517
+ return [0, nil] if params.size < (color_type == 3 ? 5 : 6)
1518
+
1519
+ c, m, y, k = channels.call
1520
+ return [0, nil] if c == -1 || m == -1 || y == -1 || (color_type == 4 && k == -1)
1521
+
1522
+ k = color_type == 3 ? 0 : k & 0xFF
1523
+ [n, cmyk_to_rgb(c & 0xFF, m & 0xFF, y & 0xFF, k)]
1524
+ when 5
1525
+ return [0, nil] if params.size < 3
1526
+ return [0, nil] unless (s_more && p_more && !more.(2)) || (!s_more && !p_more && !more.(2))
1527
+
1528
+ [3, [:ext, val.(2) & 0xFF]]
1529
+ else
1530
+ [0, nil]
1531
+ end
1532
+ end
1533
+
1534
+ # color.CMYK#RGBA followed by ansi's shift.
1535
+ def cmyk_to_rgb(c, m, y, k)
1536
+ w = 0xFFFF - (k * 0x101)
1537
+ [:rgb, *[c, m, y].map do |v|
1538
+ x = (0xFFFF - (v * 0x101)) * w / 0xFFFF
1539
+ x > 0xFF ? x >> 8 : x
1540
+ end]
1541
+ end
1542
+
1543
+ # cellbuf.ReadLink: OSC 8 data "8;params;url".
1544
+ def read_link(data, link)
1545
+ parts = data.split(";", -1)
1546
+ parts = [""] if parts.empty?
1547
+ return unless parts.size == 3
1548
+
1549
+ link[1] = parts[1]
1550
+ link[0] = parts[2]
1551
+ end
1552
+ end
1553
+ end
1554
+ end
1555
+ end