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,461 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Lipgloss join / place / width / height / size and the version strings: a port of lipgloss v1.1.0
4
+ # join.go, position.go, size.go and whitespace.go, behind the same Ruby-side argument handling as the
5
+ # lipgloss gem 0.2.2 C extension (extension.c) and its Go glue (go/layout.go).
6
+ module R2UI
7
+ module Compat
8
+ module Gloss
9
+ # Layout internals. The public entry points are the Lipgloss singleton methods defined below.
10
+ module Layout
11
+ UTF_8 = Encoding::UTF_8
12
+
13
+ # Sentinel: the gem's JSON round trip ([]string unmarshal) failed, so join returns "".
14
+ JSON_FAIL = Object.new.freeze
15
+
16
+ extend self
17
+
18
+ # --- Ruby C API argument conversions ------------------------------------------------------
19
+
20
+ # The name rb_unexpected_type / rb_convert_type print for a value.
21
+ def type_name(value)
22
+ case value
23
+ when nil then "nil"
24
+ when true then "true"
25
+ when false then "false"
26
+ else value.class.name
27
+ end
28
+ end
29
+
30
+ # Check_Type(value, T_STRING)
31
+ def check_string(value)
32
+ return if value.is_a?(String)
33
+
34
+ raise TypeError, "wrong argument type #{type_name(value)} (expected String)"
35
+ end
36
+
37
+ # Check_Type(value, T_ARRAY)
38
+ def check_array(value)
39
+ return if value.is_a?(Array)
40
+
41
+ raise TypeError, "wrong argument type #{type_name(value)} (expected Array)"
42
+ end
43
+
44
+ # StringValueCStr: implicit #to_str conversion, no NUL bytes.
45
+ def c_string(value)
46
+ str = String.try_convert(value)
47
+ raise TypeError, "no implicit conversion of #{type_name(value)} into String" if str.nil?
48
+ raise ArgumentError, "string contains null byte" if str.include?("\0")
49
+
50
+ str
51
+ end
52
+
53
+ INT_MIN = -(2**31)
54
+ INT_MAX = 2**31 - 1
55
+
56
+ # NUM2INT (rb_num2int on a 64-bit platform).
57
+ def num2int(value)
58
+ raise TypeError, "no implicit conversion from nil to integer" if value.nil?
59
+
60
+ i = case value
61
+ when Integer then value
62
+ when Float
63
+ unless value.finite? && value < 9_223_372_036_854_775_808.0 && value >= -9_223_372_036_854_775_808.0
64
+ raise RangeError, "float #{float_out_of_range_repr(value)} out of range of integer"
65
+ end
66
+
67
+ value.to_i
68
+ else
69
+ conv = Integer.try_convert(value)
70
+ raise TypeError, "no implicit conversion of #{type_name(value)} into Integer" if conv.nil?
71
+
72
+ conv
73
+ end
74
+ if i.bit_length > 63
75
+ raise RangeError, "#{i.negative? ? 'bignum too small' : 'bignum too big'} to convert into 'long'"
76
+ end
77
+ raise RangeError, "integer #{i} too small to convert to 'int'" if i < INT_MIN
78
+ raise RangeError, "integer #{i} too big to convert to 'int'" if i > INT_MAX
79
+
80
+ i
81
+ end
82
+
83
+ # NUM2DBL (rb_num2dbl).
84
+ def num2dbl(value)
85
+ case value
86
+ when Float then value
87
+ when Integer then value.to_f
88
+ when String then raise TypeError, "no implicit conversion to float from string"
89
+ when nil then raise TypeError, "no implicit conversion to float from nil"
90
+ when true then raise TypeError, "no implicit conversion to float from true"
91
+ when false then raise TypeError, "no implicit conversion to float from false"
92
+ else
93
+ raise TypeError, "can't convert #{type_name(value)} into Float" unless value.respond_to?(:to_f)
94
+
95
+ f = value.to_f
96
+ unless f.is_a?(Float)
97
+ raise TypeError, "can't convert #{type_name(value)} to Float (#{type_name(value)}#to_f gives #{type_name(f)})"
98
+ end
99
+
100
+ f
101
+ end
102
+ end
103
+
104
+ def float_out_of_range_repr(f)
105
+ return f.to_s if f.nan? || f.infinite?
106
+
107
+ format("%-.10g", f)
108
+ end
109
+
110
+ # rb_utf8_str_new_cstr(C.CString(result)): a fresh UTF-8 string, cut at the first NUL.
111
+ def c_result(str)
112
+ s = String.new(str, encoding: UTF_8)
113
+ nul = s.b.index("\0")
114
+ s = s.byteslice(0, nul).force_encoding(UTF_8) if nul
115
+ s
116
+ end
117
+
118
+ # The gem passes the array to Go as JSON (Array#to_json) and unmarshals it into []string. A
119
+ # JSON string element survives, null becomes "", anything else fails the whole unmarshal.
120
+ # Objects without a JSON representation of their own serialize as their #to_s string.
121
+ def json_strings(array)
122
+ array.map do |e|
123
+ case e
124
+ when String then e
125
+ when nil then ""
126
+ when Symbol then e.to_s
127
+ when Numeric, true, false, Array, Hash then return JSON_FAIL
128
+ else e.to_s
129
+ end
130
+ end
131
+ end
132
+
133
+ # --- lipgloss v1.1.0 ----------------------------------------------------------------------
134
+
135
+ def str_width(s) = Text.string_width(s)
136
+
137
+ # Position.value(): clamped to [0, 1].
138
+ def pos_value(pos)
139
+ [1.0, [0.0, pos].max].min
140
+ end
141
+
142
+ # get.go getLines
143
+ def get_lines(s)
144
+ lines = s.split("\n", -1)
145
+ lines = [""] if lines.empty?
146
+ widest = 0
147
+ lines.each do |l|
148
+ w = str_width(l)
149
+ widest = w if w > widest
150
+ end
151
+ [lines, widest]
152
+ end
153
+
154
+ def join_horizontal(pos, strs)
155
+ return "" if strs.empty?
156
+ return strs[0] if strs.length == 1
157
+
158
+ blocks = []
159
+ max_widths = []
160
+ max_height = 0
161
+ strs.each do |str|
162
+ lines, w = get_lines(str)
163
+ blocks << lines
164
+ max_widths << w
165
+ max_height = lines.length if lines.length > max_height
166
+ end
167
+
168
+ blocks.map! do |block|
169
+ next block if block.length >= max_height
170
+
171
+ n = max_height - block.length
172
+ extra = Array.new(n, "")
173
+ if pos == 0.0
174
+ block + extra
175
+ elsif pos == 1.0
176
+ extra + block
177
+ else
178
+ split = (n * pos_value(pos)).round
179
+ top = n - split
180
+ bottom = n - top
181
+ Array.new(n - top, "") + block + Array.new(n - bottom, "")
182
+ end
183
+ end
184
+
185
+ b = +""
186
+ rows = blocks[0].length
187
+ rows.times do |i|
188
+ blocks.each_with_index do |block, j|
189
+ b << block[i]
190
+ pad = max_widths[j] - str_width(block[i])
191
+ b << (" " * pad) if pad.positive?
192
+ end
193
+ b << "\n" if i < rows - 1
194
+ end
195
+ b
196
+ end
197
+
198
+ def join_vertical(pos, strs)
199
+ return "" if strs.empty?
200
+ return strs[0] if strs.length == 1
201
+
202
+ blocks = []
203
+ max_width = 0
204
+ strs.each do |str|
205
+ lines, w = get_lines(str)
206
+ blocks << lines
207
+ max_width = w if w > max_width
208
+ end
209
+
210
+ b = +""
211
+ blocks.each_with_index do |block, i|
212
+ block.each_with_index do |line, j|
213
+ w = max_width - str_width(line)
214
+ if pos == 0.0
215
+ b << line << spaces(w)
216
+ elsif pos == 1.0
217
+ b << spaces(w) << line
218
+ elsif w < 1
219
+ b << line
220
+ else
221
+ split = (w * pos_value(pos)).round
222
+ right = w - split
223
+ left = w - right
224
+ b << spaces(left) << line << spaces(right)
225
+ end
226
+ b << "\n" unless i == blocks.length - 1 && j == block.length - 1
227
+ end
228
+ end
229
+ b
230
+ end
231
+
232
+ def spaces(n) = n.positive? ? " " * n : ""
233
+
234
+ def width(str)
235
+ w = 0
236
+ str.split("\n", -1).each do |l|
237
+ lw = str_width(l)
238
+ w = lw if lw > w
239
+ end
240
+ w
241
+ end
242
+
243
+ def height(str) = str.count("\n") + 1
244
+
245
+ def place(width, height, hpos, vpos, str, ws)
246
+ place_vertical(height, vpos, place_horizontal(width, hpos, str, ws), ws)
247
+ end
248
+
249
+ def place_horizontal(width, pos, str, ws)
250
+ lines, content_width = get_lines(str)
251
+ gap = width - content_width
252
+ return str if gap <= 0
253
+
254
+ b = +""
255
+ lines.each_with_index do |l, i|
256
+ short = [0, content_width - str_width(l)].max
257
+ total = gap + short
258
+ if pos == 0.0
259
+ b << l << ws.render(total)
260
+ elsif pos == 1.0
261
+ b << ws.render(total) << l
262
+ else
263
+ split = (total * pos_value(pos)).round
264
+ left = total - split
265
+ right = total - left
266
+ b << ws.render(left) << l << ws.render(right)
267
+ end
268
+ b << "\n" if i < lines.length - 1
269
+ end
270
+ b
271
+ end
272
+
273
+ def place_vertical(height, pos, str, ws)
274
+ content_height = str.count("\n") + 1
275
+ gap = height - content_height
276
+ return str if gap <= 0
277
+
278
+ _, width = get_lines(str)
279
+ empty_line = ws.render(width)
280
+ b = +""
281
+ if pos == 0.0
282
+ b << str << "\n"
283
+ gap.times do |i|
284
+ b << empty_line
285
+ b << "\n" if i < gap - 1
286
+ end
287
+ elsif pos == 1.0
288
+ b << ((empty_line + "\n") * gap) << str
289
+ else
290
+ split = (gap * pos_value(pos)).round
291
+ top = gap - split
292
+ bottom = gap - top
293
+ b << ((empty_line + "\n") * top) << str
294
+ bottom.times { b << "\n" << empty_line }
295
+ end
296
+ b
297
+ end
298
+
299
+ # whitespace.go: fills gaps with (optionally styled) characters.
300
+ class Whitespace
301
+ def initialize(chars: "", foreground: nil)
302
+ @chars = chars
303
+ @foreground = foreground
304
+ end
305
+
306
+ # Built at render time, as Go builds it inside Place (newWhitespace).
307
+ def style
308
+ r = Renderer.default
309
+ s = r.color_profile.string
310
+ s = s.foreground(@foreground.color(r)) if @foreground
311
+ s
312
+ end
313
+
314
+ def render(width)
315
+ chars = @chars.empty? ? " " : @chars
316
+ runes = chars.dup.force_encoding(UTF_8).each_char.map { |c| c.valid_encoding? ? c : "�" }
317
+ j = 0
318
+ b = +""
319
+ i = 0
320
+ while i < width
321
+ b << runes[j]
322
+ j += 1
323
+ j = 0 if j >= runes.length
324
+ i += Text.string_width(runes[j])
325
+ end
326
+ short = width - Text.string_width(b)
327
+ b << (" " * short) if short.positive?
328
+
329
+ s = style
330
+ s.styled(b)
331
+ end
332
+ end
333
+
334
+ PLAIN_WHITESPACE = Whitespace.new.freeze
335
+
336
+ # The whitespace options lipgloss_place_rb reads from its keyword hash, converted in the C
337
+ # extension's order. Returns [whitespace, light, dark] where light/dark are still to be
338
+ # converted (after the numeric arguments) for an adaptive foreground.
339
+ def whitespace_options(opts)
340
+ ws_chars_v = opts[:whitespace_chars]
341
+ ws_fg_v = opts[:whitespace_foreground]
342
+ ws_chars = ws_chars_v.nil? ? "" : c_string(ws_chars_v)
343
+
344
+ if !ws_fg_v.nil? && ws_fg_v.respond_to?(:light) && ws_fg_v.respond_to?(:dark)
345
+ [:adaptive, ws_chars, ws_fg_v.light, ws_fg_v.dark]
346
+ else
347
+ ws_fg = ws_fg_v.nil? ? "" : c_string(ws_fg_v)
348
+ fg = ws_fg.empty? ? nil : TerminalColor::Color.new(ws_fg.dup)
349
+ [:plain, ws_chars, fg]
350
+ end
351
+ end
352
+ end
353
+ end
354
+ end
355
+ end
356
+
357
+ module Lipgloss
358
+ class << self
359
+ def _join_horizontal(position, strings)
360
+ layout = R2UI::Compat::Gloss::Layout
361
+ layout.check_array(strings)
362
+ strs = layout.json_strings(strings)
363
+ pos = layout.num2dbl(position)
364
+ return layout.c_result("") if strs.equal?(layout::JSON_FAIL)
365
+
366
+ layout.c_result(layout.join_horizontal(pos, strs))
367
+ end
368
+
369
+ def _join_vertical(position, strings)
370
+ layout = R2UI::Compat::Gloss::Layout
371
+ layout.check_array(strings)
372
+ strs = layout.json_strings(strings)
373
+ pos = layout.num2dbl(position)
374
+ return layout.c_result("") if strs.equal?(layout::JSON_FAIL)
375
+
376
+ layout.c_result(layout.join_vertical(pos, strs))
377
+ end
378
+
379
+ def width(string)
380
+ layout = R2UI::Compat::Gloss::Layout
381
+ layout.check_string(string)
382
+ layout.width(layout.c_string(string))
383
+ end
384
+
385
+ def height(string)
386
+ layout = R2UI::Compat::Gloss::Layout
387
+ layout.check_string(string)
388
+ layout.height(layout.c_string(string))
389
+ end
390
+
391
+ def size(string)
392
+ layout = R2UI::Compat::Gloss::Layout
393
+ layout.check_string(string)
394
+ s = layout.c_string(string)
395
+ [layout.width(s), layout.height(s)]
396
+ end
397
+
398
+ def _place(*args, **opts)
399
+ unless args.length == 5
400
+ raise ArgumentError, "wrong number of arguments (given #{args.length}, expected 5)"
401
+ end
402
+
403
+ layout = R2UI::Compat::Gloss::Layout
404
+ width, height, hpos, vpos, string = args
405
+ layout.check_string(string)
406
+
407
+ ws = layout::PLAIN_WHITESPACE
408
+ unless opts.empty?
409
+ kind, chars, *rest = layout.whitespace_options(opts)
410
+ if kind == :adaptive
411
+ light, dark = rest
412
+ w = layout.num2int(width)
413
+ h = layout.num2int(height)
414
+ hp = layout.num2dbl(hpos)
415
+ vp = layout.num2dbl(vpos)
416
+ str = layout.c_string(string)
417
+ light = layout.c_string(light)
418
+ dark = layout.c_string(dark)
419
+ fg = (light.empty? && dark.empty?) ? nil : R2UI::Compat::Gloss::TerminalColor::AdaptiveColor.new(light.dup, dark.dup)
420
+ ws = layout::Whitespace.new(chars: chars.dup, foreground: fg)
421
+ return layout.c_result(layout.place(w, h, hp, vp, str, ws))
422
+ end
423
+
424
+ ws = layout::Whitespace.new(chars: chars.dup, foreground: rest[0])
425
+ end
426
+
427
+ w = layout.num2int(width)
428
+ h = layout.num2int(height)
429
+ hp = layout.num2dbl(hpos)
430
+ vp = layout.num2dbl(vpos)
431
+ str = layout.c_string(string)
432
+ layout.c_result(layout.place(w, h, hp, vp, str, ws))
433
+ end
434
+
435
+ def _place_horizontal(width, position, string)
436
+ layout = R2UI::Compat::Gloss::Layout
437
+ layout.check_string(string)
438
+ w = layout.num2int(width)
439
+ pos = layout.num2dbl(position)
440
+ str = layout.c_string(string)
441
+ layout.c_result(layout.place_horizontal(w, pos, str, layout::PLAIN_WHITESPACE))
442
+ end
443
+
444
+ def _place_vertical(height, position, string)
445
+ layout = R2UI::Compat::Gloss::Layout
446
+ layout.check_string(string)
447
+ h = layout.num2int(height)
448
+ pos = layout.num2dbl(position)
449
+ str = layout.c_string(string)
450
+ layout.c_result(layout.place_vertical(h, pos, str, layout::PLAIN_WHITESPACE))
451
+ end
452
+
453
+ def upstream_version
454
+ String.new(R2UI::Compat::Gloss::UPSTREAM_VERSION, encoding: Encoding::UTF_8)
455
+ end
456
+
457
+ def version
458
+ format("lipgloss v%s (upstream %s) [Go native extension]", const_get(:VERSION), upstream_version)
459
+ end
460
+ end
461
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "tree/go_tree"
4
+
5
+ module Lipgloss
6
+ # Lipgloss::List as the gem's C extension (list.c) defines it over Go lipgloss list.List (a tree.Tree
7
+ # with list enumerators). Every method mutates the underlying list and returns a new Lipgloss::List
8
+ # wrapping that same list.
9
+ class List
10
+ ENUMERATORS = {
11
+ bullet: R2UI::Compat::Gloss::GoTree::ListEnumerators::BULLET,
12
+ arabic: R2UI::Compat::Gloss::GoTree::ListEnumerators::ARABIC_ENUM,
13
+ alphabet: R2UI::Compat::Gloss::GoTree::ListEnumerators::ALPHABET,
14
+ roman: R2UI::Compat::Gloss::GoTree::ListEnumerators::ROMAN_ENUM,
15
+ dash: R2UI::Compat::Gloss::GoTree::ListEnumerators::DASH,
16
+ asterisk: R2UI::Compat::Gloss::GoTree::ListEnumerators::ASTERISK
17
+ }.freeze
18
+ private_constant :ENUMERATORS
19
+
20
+ def initialize(*items)
21
+ node = go_tree
22
+ return if items.empty?
23
+
24
+ strs = R2UI::Compat::Gloss::GoTree.json_string_array(items)
25
+ strs&.each { |s| node.child(s) }
26
+ end
27
+
28
+ def item(item)
29
+ node = go_tree
30
+ if item.is_a?(Lipgloss::List)
31
+ node.child(item.send(:go_tree))
32
+ else
33
+ node.child(R2UI::Compat::Gloss::GoTree.c_string_arg(item))
34
+ end
35
+ wrap(node)
36
+ end
37
+
38
+ def items(items)
39
+ node = go_tree
40
+ strs = R2UI::Compat::Gloss::GoTree.json_string_array(items)
41
+ strs&.each { |s| node.child(s) }
42
+ wrap(node)
43
+ end
44
+
45
+ def enumerator(enum_symbol)
46
+ node = go_tree
47
+ enum = ENUMERATORS.find { |sym, _| sym.equal?(enum_symbol) }&.last
48
+ node.ensure_renderer.enumerator = enum || ENUMERATORS[:bullet]
49
+ wrap(node)
50
+ end
51
+
52
+ def enumerator_style(style)
53
+ node = go_tree
54
+ node.ensure_renderer.enumerator_style = R2UI::Compat::Gloss::GoTree.style_arg(style)
55
+ wrap(node)
56
+ end
57
+
58
+ def item_style(style)
59
+ node = go_tree
60
+ node.ensure_renderer.item_style = R2UI::Compat::Gloss::GoTree.style_arg(style)
61
+ wrap(node)
62
+ end
63
+
64
+ def render
65
+ R2UI::Compat::Gloss::GoTree.render(go_tree)
66
+ end
67
+
68
+ def to_s
69
+ render
70
+ end
71
+
72
+ private
73
+
74
+ # The gem allocates the Go list (list.New()) in the alloc function.
75
+ def go_tree
76
+ @tree ||= R2UI::Compat::Gloss::GoTree.new_list
77
+ end
78
+
79
+ def wrap(node)
80
+ list = self.class.allocate
81
+ list.instance_variable_set(:@tree, node)
82
+ list
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Same as lipgloss-ruby 0.2.2 lib/lipgloss/position.rb (MIT).
4
+ module Lipgloss
5
+ # Positions are floats from 0.0 (top/left) to 1.0 (bottom/right); 0.5 is center.
6
+ module Position
7
+ TOP = 0.0
8
+ BOTTOM = 1.0
9
+ LEFT = 0.0
10
+ RIGHT = 1.0
11
+ CENTER = 0.5
12
+
13
+ SYMBOLS = {
14
+ top: TOP,
15
+ bottom: BOTTOM,
16
+ left: LEFT,
17
+ right: RIGHT,
18
+ center: CENTER
19
+ }.freeze
20
+
21
+ def self.resolve(value)
22
+ case value
23
+ when Symbol then SYMBOLS.fetch(value) { raise ArgumentError, "Unknown position: #{value.inspect}" }
24
+ when String then SYMBOLS.fetch(value.to_sym) { raise ArgumentError, "Unknown position: #{value.inspect}" }
25
+ when Numeric then value.to_f
26
+ else raise ArgumentError, "Position must be a Symbol or Numeric, got #{value.class}"
27
+ end
28
+ end
29
+ end
30
+ end