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,424 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+ require_relative "width_table"
5
+
6
+ module R2UI
7
+ module Compat
8
+ module Tea
9
+ # Ports of charmbracelet/x/ansi v0.8.0 StringWidth and Truncate, which
10
+ # the bubbletea 0.1.4 gem's renderer uses. Escape sequences are found
11
+ # with the same DEC ANSI transition table; text is measured per
12
+ # grapheme cluster the way rivo/uniseg v0.4.7 measures it.
13
+ module ANSI
14
+ # Parser states (x/ansi/parser/const.go).
15
+ GROUND = 0
16
+ CSI_ENTRY = 1
17
+ CSI_INTERMEDIATE = 2
18
+ CSI_PARAM = 3
19
+ DCS_ENTRY = 4
20
+ DCS_INTERMEDIATE = 5
21
+ DCS_PARAM = 6
22
+ DCS_STRING = 7
23
+ ESCAPE = 8
24
+ ESCAPE_INTERMEDIATE = 9
25
+ OSC_STRING = 10
26
+ SOS_STRING = 11
27
+ PM_STRING = 12
28
+ APC_STRING = 13
29
+ UTF8 = 14
30
+
31
+ # Parser actions.
32
+ NONE = 0
33
+ CLEAR = 1
34
+ COLLECT = 2
35
+ PREFIX = 3
36
+ DISPATCH = 4
37
+ EXECUTE = 5
38
+ START = 6
39
+ PUT = 7
40
+ PARAM = 8
41
+ PRINT = 9
42
+ IGNORE = NONE
43
+
44
+ CURSOR_HOME_POSITION = "\e[H"
45
+ ERASE_ENTIRE_SCREEN = "\e[2J"
46
+
47
+ VS15 = 0xFE0E
48
+ VS16 = 0xFE0F
49
+
50
+ # x/ansi parser.GenerateTransitionTable, value = action << 4 | state,
51
+ # indexed by state << 8 | byte.
52
+ TABLE = begin
53
+ t = Array.new(4096, (NONE << 4) | GROUND)
54
+ one = ->(code, state, action, nxt) { t[(state << 8) | code] = (action << 4) | nxt }
55
+ many = ->(codes, state, action, nxt) { codes.each { |c| one.(c, state, action, nxt) } }
56
+
57
+ (GROUND..UTF8).each do |s|
58
+ many.([0x18, 0x1A, 0x99, 0x9A], s, EXECUTE, GROUND)
59
+ many.(0x80..0x8F, s, EXECUTE, GROUND)
60
+ many.(0x90..0x97, s, EXECUTE, GROUND)
61
+ one.(0x9C, s, EXECUTE, GROUND)
62
+ one.(0x1B, s, CLEAR, ESCAPE)
63
+ one.(0x98, s, START, SOS_STRING)
64
+ one.(0x9E, s, START, PM_STRING)
65
+ one.(0x9F, s, START, APC_STRING)
66
+ one.(0x9B, s, CLEAR, CSI_ENTRY)
67
+ one.(0x90, s, CLEAR, DCS_ENTRY)
68
+ one.(0x9D, s, START, OSC_STRING)
69
+ many.(0xC2..0xDF, s, COLLECT, UTF8)
70
+ many.(0xE0..0xEF, s, COLLECT, UTF8)
71
+ many.(0xF0..0xF4, s, COLLECT, UTF8)
72
+ end
73
+
74
+ many.(0x00..0x17, GROUND, EXECUTE, GROUND)
75
+ one.(0x19, GROUND, EXECUTE, GROUND)
76
+ many.(0x1C..0x1F, GROUND, EXECUTE, GROUND)
77
+ many.(0x20..0x7E, GROUND, PRINT, GROUND)
78
+ one.(0x7F, GROUND, EXECUTE, GROUND)
79
+
80
+ many.(0x00..0x17, ESCAPE_INTERMEDIATE, EXECUTE, ESCAPE_INTERMEDIATE)
81
+ one.(0x19, ESCAPE_INTERMEDIATE, EXECUTE, ESCAPE_INTERMEDIATE)
82
+ many.(0x1C..0x1F, ESCAPE_INTERMEDIATE, EXECUTE, ESCAPE_INTERMEDIATE)
83
+ many.(0x20..0x2F, ESCAPE_INTERMEDIATE, COLLECT, ESCAPE_INTERMEDIATE)
84
+ one.(0x7F, ESCAPE_INTERMEDIATE, IGNORE, ESCAPE_INTERMEDIATE)
85
+ many.(0x30..0x7E, ESCAPE_INTERMEDIATE, DISPATCH, GROUND)
86
+
87
+ many.(0x00..0x17, ESCAPE, EXECUTE, ESCAPE)
88
+ one.(0x19, ESCAPE, EXECUTE, ESCAPE)
89
+ many.(0x1C..0x1F, ESCAPE, EXECUTE, ESCAPE)
90
+ one.(0x7F, ESCAPE, IGNORE, ESCAPE)
91
+ many.(0x30..0x4F, ESCAPE, DISPATCH, GROUND)
92
+ many.(0x51..0x57, ESCAPE, DISPATCH, GROUND)
93
+ one.(0x59, ESCAPE, DISPATCH, GROUND)
94
+ one.(0x5A, ESCAPE, DISPATCH, GROUND)
95
+ one.(0x5C, ESCAPE, DISPATCH, GROUND)
96
+ many.(0x60..0x7E, ESCAPE, DISPATCH, GROUND)
97
+ many.(0x20..0x2F, ESCAPE, COLLECT, ESCAPE_INTERMEDIATE)
98
+ one.(0x58, ESCAPE, START, SOS_STRING)
99
+ one.(0x5E, ESCAPE, START, PM_STRING)
100
+ one.(0x5F, ESCAPE, START, APC_STRING)
101
+ one.(0x50, ESCAPE, CLEAR, DCS_ENTRY)
102
+ one.(0x5B, ESCAPE, CLEAR, CSI_ENTRY)
103
+ one.(0x5D, ESCAPE, START, OSC_STRING)
104
+
105
+ (SOS_STRING..APC_STRING).each do |s|
106
+ many.(0x00..0x17, s, PUT, s)
107
+ one.(0x19, s, PUT, s)
108
+ many.(0x1C..0x1F, s, PUT, s)
109
+ many.(0x20..0x7F, s, PUT, s)
110
+ one.(0x1B, s, DISPATCH, ESCAPE)
111
+ one.(0x9C, s, DISPATCH, GROUND)
112
+ many.([0x18, 0x1A], s, IGNORE, GROUND)
113
+ end
114
+
115
+ many.(0x00..0x07, DCS_ENTRY, IGNORE, DCS_ENTRY)
116
+ many.(0x0E..0x17, DCS_ENTRY, IGNORE, DCS_ENTRY)
117
+ one.(0x19, DCS_ENTRY, IGNORE, DCS_ENTRY)
118
+ many.(0x1C..0x1F, DCS_ENTRY, IGNORE, DCS_ENTRY)
119
+ one.(0x7F, DCS_ENTRY, IGNORE, DCS_ENTRY)
120
+ many.(0x20..0x2F, DCS_ENTRY, COLLECT, DCS_INTERMEDIATE)
121
+ many.(0x30..0x3B, DCS_ENTRY, PARAM, DCS_PARAM)
122
+ many.(0x3C..0x3F, DCS_ENTRY, PREFIX, DCS_PARAM)
123
+ many.(0x08..0x0D, DCS_ENTRY, PUT, DCS_STRING)
124
+ one.(0x1B, DCS_ENTRY, PUT, DCS_STRING)
125
+ many.(0x40..0x7E, DCS_ENTRY, START, DCS_STRING)
126
+
127
+ many.(0x00..0x17, DCS_INTERMEDIATE, IGNORE, DCS_INTERMEDIATE)
128
+ one.(0x19, DCS_INTERMEDIATE, IGNORE, DCS_INTERMEDIATE)
129
+ many.(0x1C..0x1F, DCS_INTERMEDIATE, IGNORE, DCS_INTERMEDIATE)
130
+ many.(0x20..0x2F, DCS_INTERMEDIATE, COLLECT, DCS_INTERMEDIATE)
131
+ one.(0x7F, DCS_INTERMEDIATE, IGNORE, DCS_INTERMEDIATE)
132
+ many.(0x30..0x3F, DCS_INTERMEDIATE, START, DCS_STRING)
133
+ many.(0x40..0x7E, DCS_INTERMEDIATE, START, DCS_STRING)
134
+
135
+ many.(0x00..0x17, DCS_PARAM, IGNORE, DCS_PARAM)
136
+ one.(0x19, DCS_PARAM, IGNORE, DCS_PARAM)
137
+ many.(0x1C..0x1F, DCS_PARAM, IGNORE, DCS_PARAM)
138
+ many.(0x30..0x3B, DCS_PARAM, PARAM, DCS_PARAM)
139
+ one.(0x7F, DCS_PARAM, IGNORE, DCS_PARAM)
140
+ many.(0x3C..0x3F, DCS_PARAM, IGNORE, DCS_PARAM)
141
+ many.(0x20..0x2F, DCS_PARAM, COLLECT, DCS_INTERMEDIATE)
142
+ many.(0x40..0x7E, DCS_PARAM, START, DCS_STRING)
143
+
144
+ many.(0x00..0x17, DCS_STRING, PUT, DCS_STRING)
145
+ one.(0x19, DCS_STRING, PUT, DCS_STRING)
146
+ many.(0x1C..0x1F, DCS_STRING, PUT, DCS_STRING)
147
+ many.(0x20..0x7E, DCS_STRING, PUT, DCS_STRING)
148
+ one.(0x7F, DCS_STRING, PUT, DCS_STRING)
149
+ many.(0x80..0xFF, DCS_STRING, PUT, DCS_STRING)
150
+ one.(0x1B, DCS_STRING, DISPATCH, ESCAPE)
151
+ one.(0x9C, DCS_STRING, DISPATCH, GROUND)
152
+ many.([0x18, 0x1A], DCS_STRING, IGNORE, GROUND)
153
+
154
+ many.(0x00..0x17, CSI_PARAM, EXECUTE, CSI_PARAM)
155
+ one.(0x19, CSI_PARAM, EXECUTE, CSI_PARAM)
156
+ many.(0x1C..0x1F, CSI_PARAM, EXECUTE, CSI_PARAM)
157
+ many.(0x30..0x3B, CSI_PARAM, PARAM, CSI_PARAM)
158
+ one.(0x7F, CSI_PARAM, IGNORE, CSI_PARAM)
159
+ many.(0x3C..0x3F, CSI_PARAM, IGNORE, CSI_PARAM)
160
+ many.(0x40..0x7E, CSI_PARAM, DISPATCH, GROUND)
161
+ many.(0x20..0x2F, CSI_PARAM, COLLECT, CSI_INTERMEDIATE)
162
+
163
+ many.(0x00..0x17, CSI_INTERMEDIATE, EXECUTE, CSI_INTERMEDIATE)
164
+ one.(0x19, CSI_INTERMEDIATE, EXECUTE, CSI_INTERMEDIATE)
165
+ many.(0x1C..0x1F, CSI_INTERMEDIATE, EXECUTE, CSI_INTERMEDIATE)
166
+ many.(0x20..0x2F, CSI_INTERMEDIATE, COLLECT, CSI_INTERMEDIATE)
167
+ one.(0x7F, CSI_INTERMEDIATE, IGNORE, CSI_INTERMEDIATE)
168
+ many.(0x40..0x7E, CSI_INTERMEDIATE, DISPATCH, GROUND)
169
+ many.(0x30..0x3F, CSI_INTERMEDIATE, IGNORE, GROUND)
170
+
171
+ many.(0x00..0x17, CSI_ENTRY, EXECUTE, CSI_ENTRY)
172
+ one.(0x19, CSI_ENTRY, EXECUTE, CSI_ENTRY)
173
+ many.(0x1C..0x1F, CSI_ENTRY, EXECUTE, CSI_ENTRY)
174
+ one.(0x7F, CSI_ENTRY, IGNORE, CSI_ENTRY)
175
+ many.(0x40..0x7E, CSI_ENTRY, DISPATCH, GROUND)
176
+ many.(0x20..0x2F, CSI_ENTRY, COLLECT, CSI_INTERMEDIATE)
177
+ many.(0x30..0x3B, CSI_ENTRY, PARAM, CSI_PARAM)
178
+ many.(0x3C..0x3F, CSI_ENTRY, PREFIX, CSI_PARAM)
179
+
180
+ many.(0x00..0x06, OSC_STRING, IGNORE, OSC_STRING)
181
+ many.(0x08..0x17, OSC_STRING, IGNORE, OSC_STRING)
182
+ one.(0x19, OSC_STRING, IGNORE, OSC_STRING)
183
+ many.(0x1C..0x1F, OSC_STRING, IGNORE, OSC_STRING)
184
+ many.(0x20..0xFF, OSC_STRING, PUT, OSC_STRING)
185
+ one.(0x1B, OSC_STRING, DISPATCH, ESCAPE)
186
+ one.(0x07, OSC_STRING, DISPATCH, GROUND)
187
+ one.(0x9C, OSC_STRING, DISPATCH, GROUND)
188
+ many.([0x18, 0x1A], OSC_STRING, IGNORE, GROUND)
189
+
190
+ t.freeze
191
+ end
192
+
193
+ PLAIN = /[^\x20-\x7E]/n
194
+ PLAIN_UTF8 = /[^\x20-\x7E]/
195
+ GRAPHEME = /\X/m
196
+ FFFD = "\u{FFFD}"
197
+ # Bounded memo tables: codepoint => width for one-rune clusters and
198
+ # cluster bytes => width for longer ones (both repeat heavily).
199
+ CACHE_LIMIT = 4096
200
+ RUNE_WIDTHS = {} # rubocop:disable Style/MutableConstant
201
+ CLUSTER_WIDTHS = {} # rubocop:disable Style/MutableConstant
202
+ private_constant :PLAIN, :PLAIN_UTF8, :GRAPHEME, :FFFD, :CACHE_LIMIT, :RUNE_WIDTHS, :CLUSTER_WIDTHS
203
+
204
+ module_function
205
+
206
+ # x/ansi StringWidth: cells the string occupies; escape sequences
207
+ # are zero-width.
208
+ def string_width(str)
209
+ return 0 if str.empty?
210
+
211
+ s = utf8_view(str)
212
+ valid = s.valid_encoding?
213
+ if valid
214
+ return s.bytesize unless s.match?(PLAIN_UTF8)
215
+ else
216
+ s = s.b
217
+ return s.bytesize unless s.match?(PLAIN)
218
+ end
219
+ scanner = nil
220
+
221
+ pstate = GROUND
222
+ width = 0
223
+ i = 0
224
+ n = s.bytesize
225
+ while i < n
226
+ v = TABLE[(pstate << 8) | s.getbyte(i)]
227
+ state = v & 15
228
+ if state == UTF8
229
+ if valid
230
+ scanner ||= StringScanner.new(s)
231
+ scanner.pos = i
232
+ len = scanner.skip(GRAPHEME)
233
+ w = valid_cluster_width(s, i, len)
234
+ else
235
+ len, w = first_grapheme_cluster(s, i)
236
+ end
237
+ width += w
238
+ i += len
239
+ pstate = GROUND
240
+ next
241
+ end
242
+ width += 1 if (v >> 4) == PRINT
243
+ pstate = state
244
+ i += 1
245
+ end
246
+ width
247
+ end
248
+
249
+ # The string's bytes read as UTF-8 (what Go's string is), copying
250
+ # only when its encoding says otherwise.
251
+ def utf8_view(str)
252
+ return str if str.encoding == Encoding::UTF_8 || str.ascii_only?
253
+
254
+ str.b.force_encoding(Encoding::UTF_8)
255
+ end
256
+
257
+ # Width of the cluster at byte i of valid UTF-8 `s`, `len` bytes
258
+ # long; i is a lead byte (0xC2..0xF4).
259
+ def valid_cluster_width(s, i, len)
260
+ lead = s.getbyte(i)
261
+ if lead < 0xE0
262
+ if len == 2
263
+ return rune_width(((lead & 0x1F) << 6) | (s.getbyte(i + 1) & 0x3F))
264
+ end
265
+ elsif lead < 0xF0
266
+ if len == 3
267
+ return rune_width(((lead & 0x0F) << 12) | ((s.getbyte(i + 1) & 0x3F) << 6) |
268
+ (s.getbyte(i + 2) & 0x3F))
269
+ end
270
+ elsif len == 4
271
+ return rune_width(((lead & 0x07) << 18) | ((s.getbyte(i + 1) & 0x3F) << 12) |
272
+ ((s.getbyte(i + 2) & 0x3F) << 6) | (s.getbyte(i + 3) & 0x3F))
273
+ end
274
+
275
+ cluster = s.byteslice(i, len)
276
+ CLUSTER_WIDTHS.fetch(cluster) do
277
+ CLUSTER_WIDTHS.clear if CLUSTER_WIDTHS.size >= CACHE_LIMIT
278
+ CLUSTER_WIDTHS[cluster] = cluster_width(cluster.codepoints)
279
+ end
280
+ end
281
+
282
+ def rune_width(cp)
283
+ RUNE_WIDTHS.fetch(cp) do
284
+ RUNE_WIDTHS.clear if RUNE_WIDTHS.size >= CACHE_LIMIT
285
+ RUNE_WIDTHS[cp] = WidthTable.width(cp)
286
+ end
287
+ end
288
+
289
+ # x/ansi Truncate(s, length, ""): cut the string to at most `length`
290
+ # cells, keeping every escape sequence (including those after the cut).
291
+ def truncate(str, length, tail = "")
292
+ return str if string_width(str) <= length
293
+
294
+ length -= string_width(tail)
295
+ return +"" if length.negative?
296
+
297
+ b = str.b
298
+ u = utf8_view(str)
299
+ scanner = u.valid_encoding? ? StringScanner.new(u) : nil
300
+ buf = +"".b
301
+ tail = tail.b
302
+ cur_width = 0
303
+ ignoring = false
304
+ pstate = GROUND
305
+ i = 0
306
+ n = b.bytesize
307
+ while i < n
308
+ byte = b.getbyte(i)
309
+ v = TABLE[(pstate << 8) | byte]
310
+ state = v & 15
311
+ if state == UTF8
312
+ if scanner
313
+ scanner.pos = i
314
+ len = scanner.skip(GRAPHEME)
315
+ width = valid_cluster_width(u, i, len)
316
+ else
317
+ len, width = first_grapheme_cluster(b, i)
318
+ end
319
+ cluster_start = i
320
+ i += len
321
+ next if ignoring
322
+
323
+ if cur_width + width > length
324
+ ignoring = true
325
+ buf << tail
326
+ next
327
+ end
328
+
329
+ cur_width += width
330
+ buf << b.byteslice(cluster_start, len)
331
+ pstate = GROUND
332
+ next
333
+ end
334
+
335
+ if (v >> 4) == PRINT
336
+ if cur_width >= length && !ignoring
337
+ ignoring = true
338
+ buf << tail
339
+ end
340
+ if ignoring
341
+ i += 1
342
+ next
343
+ end
344
+ cur_width += 1
345
+ end
346
+ buf << byte
347
+ i += 1
348
+
349
+ pstate = state
350
+ if cur_width > length && !ignoring
351
+ ignoring = true
352
+ buf << tail
353
+ end
354
+ end
355
+
356
+ buf.force_encoding(str.encoding)
357
+ end
358
+
359
+ # uniseg FirstGraphemeCluster(b[i:], -1): byte length and width of
360
+ # the grapheme cluster starting at byte i.
361
+ def first_grapheme_cluster(bytes, i)
362
+ want = 8
363
+ loop do
364
+ runes, lens, done = decode_runes(bytes, i, want)
365
+ text = runes.pack("U*")
366
+ cluster = text[/\A\X/m]
367
+ count = cluster.length
368
+ if count < runes.length || done
369
+ return [lens.first(count).sum, cluster_width(runes.first(count))]
370
+ end
371
+
372
+ want *= 2
373
+ end
374
+ end
375
+
376
+ # Decodes up to `max` runes from byte i the way Go's utf8.DecodeRune
377
+ # does: invalid bytes become U+FFFD, one byte each.
378
+ def decode_runes(bytes, i, max)
379
+ runes = []
380
+ lens = []
381
+ n = bytes.bytesize
382
+ while i < n && runes.length < max
383
+ lead = bytes.getbyte(i)
384
+ size = if lead < 0x80 then 1
385
+ elsif lead >= 0xC2 && lead <= 0xDF then 2
386
+ elsif lead >= 0xE0 && lead <= 0xEF then 3
387
+ elsif lead >= 0xF0 && lead <= 0xF4 then 4
388
+ else 0
389
+ end
390
+ char = size.positive? && bytes.byteslice(i, size).force_encoding(Encoding::UTF_8)
391
+ if char && char.bytesize == size && char.valid_encoding?
392
+ runes << char.ord
393
+ lens << size
394
+ i += size
395
+ else
396
+ runes << 0xFFFD
397
+ lens << 1
398
+ i += 1
399
+ end
400
+ end
401
+ [runes, lens, i >= n]
402
+ end
403
+
404
+ # uniseg's width for one cluster: the first rune's width; then for
405
+ # Extended_Pictographic VS15/VS16 set 1/2, for regional indicators
406
+ # and Hangul L nothing is added, otherwise each rune's width adds.
407
+ def cluster_width(runes)
408
+ first = runes.first
409
+ width = WidthTable.width(first)
410
+ if WidthTable.extended_pictographic?(first)
411
+ runes.drop(1).each do |r|
412
+ if r == VS15 then width = 1
413
+ elsif r == VS16 then width = 2
414
+ end
415
+ end
416
+ elsif !WidthTable.width_of_first_only?(first)
417
+ runes.drop(1).each { |r| width += WidthTable.width(r) }
418
+ end
419
+ width
420
+ end
421
+ end
422
+ end
423
+ end
424
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copied unchanged from bubbletea 0.1.4 (https://github.com/marcoroth/bubbletea-ruby).
4
+ # Copyright (c) 2025 Marco Roth. MIT License; see LICENSE-bubbletea.txt in this directory.
5
+
6
+ module Bubbletea
7
+ class Command
8
+ end
9
+
10
+ class QuitCommand < Command
11
+ end
12
+
13
+ class BatchCommand < Command
14
+ attr_reader :commands
15
+
16
+ def initialize(commands)
17
+ super()
18
+
19
+ @commands = commands
20
+ end
21
+ end
22
+
23
+ class TickCommand < Command
24
+ attr_reader :duration, :callback
25
+
26
+ def initialize(duration, &callback)
27
+ super()
28
+
29
+ @duration = duration
30
+ @callback = callback
31
+ end
32
+ end
33
+
34
+ class SendMessage < Command
35
+ attr_reader :message, :delay
36
+
37
+ def initialize(message, delay: 0)
38
+ super()
39
+
40
+ @message = message
41
+ @delay = delay
42
+ end
43
+ end
44
+
45
+ class SequenceCommand < Command
46
+ attr_reader :commands
47
+
48
+ def initialize(commands)
49
+ super()
50
+
51
+ @commands = commands
52
+ end
53
+ end
54
+
55
+ class EnterAltScreenCommand < Command
56
+ end
57
+
58
+ class ExitAltScreenCommand < Command
59
+ end
60
+
61
+ class SetWindowTitleCommand < Command
62
+ attr_reader :title
63
+
64
+ def initialize(title)
65
+ super()
66
+
67
+ @title = title
68
+ end
69
+ end
70
+
71
+ class PutsCommand < Command
72
+ attr_reader :text
73
+
74
+ def initialize(text)
75
+ super()
76
+
77
+ @text = text
78
+ end
79
+ end
80
+
81
+ class SuspendCommand < Command
82
+ end
83
+
84
+ class ExecCommand < Command
85
+ attr_reader :callable, :message
86
+
87
+ def initialize(callable, message: nil)
88
+ super()
89
+ @callable = callable
90
+ @message = message
91
+ end
92
+ end
93
+
94
+ class << self
95
+ def quit
96
+ QuitCommand.new
97
+ end
98
+
99
+ def batch(*commands)
100
+ BatchCommand.new(commands.flatten.compact)
101
+ end
102
+
103
+ def tick(duration, &)
104
+ TickCommand.new(duration, &)
105
+ end
106
+
107
+ def send_message(message, delay: 0)
108
+ SendMessage.new(message, delay: delay)
109
+ end
110
+
111
+ def sequence(*commands)
112
+ SequenceCommand.new(commands.flatten.compact)
113
+ end
114
+
115
+ def none
116
+ nil
117
+ end
118
+
119
+ def enter_alt_screen
120
+ EnterAltScreenCommand.new
121
+ end
122
+
123
+ def exit_alt_screen
124
+ ExitAltScreenCommand.new
125
+ end
126
+
127
+ def set_window_title(title) # rubocop:disable Naming/AccessorMethodName
128
+ SetWindowTitleCommand.new(title)
129
+ end
130
+
131
+ def puts(text)
132
+ PutsCommand.new(text)
133
+ end
134
+
135
+ def suspend
136
+ SuspendCommand.new
137
+ end
138
+
139
+ def exec(callable, message: nil)
140
+ ExecCommand.new(callable, message: message)
141
+ end
142
+ end
143
+ end