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,518 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Table state and rendering: a port of Go lipgloss v1.1.0 table/table.go, resizing.go, rows.go and
4
+ # util.go. Lipgloss::Table (table.rb) is the gem-facing wrapper.
5
+ module R2UI
6
+ module Compat
7
+ module Gloss
8
+ class TableRender
9
+ HEADER_ROW = -1
10
+
11
+ # The Go *Table fields that persist between calls (rows.go StringData is folded in).
12
+ class State
13
+ attr_accessor :border, :border_top, :border_bottom, :border_left, :border_right, :border_header,
14
+ :border_column, :border_row, :border_style, :headers, :data_rows, :data_columns,
15
+ :width, :height, :use_manual_height, :offset, :wrap, :style_map, :widths, :heights
16
+
17
+ def initialize
18
+ @style_map = nil
19
+ @border = Borders.fetch(:rounded)
20
+ @border_top = true
21
+ @border_bottom = true
22
+ @border_left = true
23
+ @border_right = true
24
+ @border_header = true
25
+ @border_column = true
26
+ @border_row = false
27
+ @border_style = nil
28
+ @headers = []
29
+ @width = 0
30
+ @height = 0
31
+ @use_manual_height = false
32
+ @offset = 0
33
+ @wrap = true
34
+ @widths = []
35
+ @heights = []
36
+ clear_rows
37
+ end
38
+
39
+ def clear_rows
40
+ @data_rows = []
41
+ @data_columns = 0
42
+ end
43
+
44
+ # StringData.Append
45
+ def append(row)
46
+ @data_columns = row.length if row.length > @data_columns
47
+ @data_rows << row
48
+ end
49
+
50
+ # StringData.At
51
+ def at(row, cell)
52
+ r = @data_rows[row]
53
+ return "" if r.nil? || cell >= r.length
54
+
55
+ r[cell]
56
+ end
57
+ end
58
+
59
+ class << self
60
+ # [][]string unmarshal of rows.to_json: each element must be an Array of JSON strings (or null).
61
+ def json_rows(rows)
62
+ rows.map do |r|
63
+ case r
64
+ when nil then []
65
+ when Array
66
+ strs = Layout.json_strings(r)
67
+ return nil if strs.equal?(Layout::JSON_FAIL)
68
+
69
+ strs
70
+ else return nil
71
+ end
72
+ end
73
+ end
74
+
75
+ # TypedData_Get_Struct(value, ..., &style_type, ...)
76
+ def check_style(value)
77
+ return if value.is_a?(::Lipgloss::Style)
78
+
79
+ raise TypeError, "wrong argument type #{Layout.type_name(value)} (expected Lipgloss::Style)"
80
+ end
81
+
82
+ def new_style = ::Lipgloss::Style.new
83
+ end
84
+
85
+ def initialize(state)
86
+ @t = state
87
+ @default_style = self.class.new_style
88
+ @border_style = state.border_style || @default_style
89
+ @b = state.border
90
+ end
91
+
92
+ # Table.String
93
+ def render
94
+ t = @t
95
+ has_headers = !t.headers.empty?
96
+ has_rows = !t.data_rows.empty?
97
+ return +"" if !has_headers && !has_rows
98
+
99
+ if has_headers
100
+ (t.headers.length...t.data_columns).each { t.headers << "" }
101
+ end
102
+
103
+ resize
104
+
105
+ sb = +""
106
+ if t.border_top
107
+ sb << construct_top_border << "\n"
108
+ end
109
+ sb << construct_headers << "\n" if has_headers
110
+
111
+ bottom = t.border_bottom ? construct_bottom_border : +""
112
+
113
+ if has_rows
114
+ if t.use_manual_height
115
+ top_height = Layout.height(sb) - 1
116
+ available = t.height - (top_height + Layout.height(bottom))
117
+ available = t.data_rows.length if available > t.data_rows.length
118
+ sb << construct_rows(available)
119
+ else
120
+ (t.offset...t.data_rows.length).each { |r| sb << construct_row(r, false) }
121
+ end
122
+ end
123
+
124
+ sb << bottom
125
+
126
+ @default_style.max_height(compute_height).max_width(t.width).send(:go_render, sb)
127
+ end
128
+
129
+ private
130
+
131
+ def brender(str) = @border_style.send(:go_render, str)
132
+
133
+ # Table.style / the gem's map-backed StyleFunc.
134
+ def style(row, col)
135
+ map = @t.style_map
136
+ return @default_style if map.nil?
137
+
138
+ map["#{row},#{col}"] || @default_style
139
+ end
140
+
141
+ def btoi(b) = b ? 1 : 0
142
+
143
+ def compute_height
144
+ t = @t
145
+ t.heights.sum - 1 + btoi(!t.headers.empty?) + btoi(t.border_top) + btoi(t.border_bottom) +
146
+ btoi(t.border_header) + (t.data_rows.length * btoi(t.border_row))
147
+ end
148
+
149
+ def construct_top_border
150
+ s = +""
151
+ s << brender(@b.top_left) if @t.border_left
152
+ widths = @t.widths
153
+ widths.each_with_index do |w, i|
154
+ s << brender(@b.top * w)
155
+ s << brender(@b.middle_top) if i < widths.length - 1 && @t.border_column
156
+ end
157
+ s << brender(@b.top_right) if @t.border_right
158
+ s
159
+ end
160
+
161
+ def construct_bottom_border
162
+ s = +""
163
+ s << brender(@b.bottom_left) if @t.border_left
164
+ widths = @t.widths
165
+ widths.each_with_index do |w, i|
166
+ s << brender(@b.bottom * w)
167
+ s << brender(@b.middle_bottom) if i < widths.length - 1 && @t.border_column
168
+ end
169
+ s << brender(@b.bottom_right) if @t.border_right
170
+ s
171
+ end
172
+
173
+ def construct_headers
174
+ t = @t
175
+ headers = t.headers
176
+ s = +""
177
+ s << brender(@b.left) if t.border_left
178
+ headers.each_with_index do |header, i|
179
+ w = t.widths[i]
180
+ s << style(HEADER_ROW, i).max_height(1).width(w).max_width(w)
181
+ .send(:go_render, Text.truncate(header, w, "…"))
182
+ s << brender(@b.left) if i < headers.length - 1 && t.border_column
183
+ end
184
+ if t.border_header
185
+ s << brender(@b.right) if t.border_right
186
+ s << "\n"
187
+ s << brender(@b.middle_left) if t.border_left
188
+ headers.length.times do |i|
189
+ s << brender(@b.top * t.widths[i])
190
+ s << brender(@b.middle) if i < headers.length - 1 && t.border_column
191
+ end
192
+ s << brender(@b.middle_right) if t.border_right
193
+ end
194
+ s << brender(@b.right) if t.border_right && !t.border_header
195
+ s
196
+ end
197
+
198
+ def construct_rows(available_lines)
199
+ t = @t
200
+ n = t.data_rows.length
201
+ sb = +""
202
+ offset_row_count = n - t.offset
203
+ rows_to_render = [available_lines, 1].max
204
+ needs_overflow = rows_to_render < offset_row_count
205
+ row_idx = needs_overflow ? t.offset : n - rows_to_render
206
+ while rows_to_render.positive? && row_idx < n
207
+ is_overflow = needs_overflow && rows_to_render == 1
208
+ sb << construct_row(row_idx, is_overflow)
209
+ row_idx += 1
210
+ rows_to_render -= 1
211
+ end
212
+ sb
213
+ end
214
+
215
+ def construct_row(index, is_overflow)
216
+ t = @t
217
+ has_headers = !t.headers.empty?
218
+ height = t.heights.fetch(index + btoi(has_headers))
219
+ height = 1 if is_overflow
220
+
221
+ cells = []
222
+ left = (brender(@b.left) + "\n") * height
223
+ cells << left if t.border_left
224
+
225
+ columns = t.data_columns
226
+ columns.times do |c|
227
+ cell_width = t.widths[c]
228
+ cell = is_overflow ? "…" : t.at(index, c)
229
+ cs = style(index, c)
230
+ unless t.wrap
231
+ length = (cell_width * height) - cs.send(:get_horizontal_padding)
232
+ cell = Text.truncate(cell, length, "…")
233
+ end
234
+ cells << cs.height(height - cs.send(:get_vertical_margins))
235
+ .max_height(height)
236
+ .width(cell_width - cs.send(:get_horizontal_margins))
237
+ .max_width(cell_width)
238
+ .send(:go_render, cell)
239
+ cells << left if c < columns - 1 && t.border_column
240
+ end
241
+
242
+ cells << ((brender(@b.right) + "\n") * height) if t.border_right
243
+
244
+ # Right-trim newlines with a byte scan; /\n+\z/ backtracks quadratically.
245
+ cells.map! do |cell|
246
+ i = cell.bytesize
247
+ i -= 1 while i.positive? && cell.getbyte(i - 1) == 10
248
+ i == cell.bytesize ? cell : cell.byteslice(0, i)
249
+ end
250
+
251
+ s = +""
252
+ s << Layout.join_horizontal(0.0, cells) << "\n"
253
+
254
+ if t.border_row && index < t.data_rows.length - 1
255
+ s << brender(@b.middle_left)
256
+ t.widths.each_with_index do |w, i|
257
+ s << brender(@b.bottom * w)
258
+ s << brender(@b.middle) if i < t.widths.length - 1 && t.border_column
259
+ end
260
+ s << brender(@b.middle_right) << "\n"
261
+ end
262
+ s
263
+ end
264
+
265
+ # --- resizing.go ---------------------------------------------------------------------------
266
+
267
+ Column = Struct.new(:index, :min, :max, :median, :rows, :x_padding, :fixed_width)
268
+
269
+ def resize
270
+ t = @t
271
+ has_headers = !t.headers.empty?
272
+ ncols = t.data_columns
273
+ rows = t.data_rows.each_index.map { |i| Array.new(ncols) { |j| t.at(i, j) } }
274
+ all_rows = has_headers ? [t.headers] + rows : rows
275
+
276
+ r = Resizer.new(t.width, t.headers, all_rows)
277
+ r.wrap = t.wrap
278
+ r.border_column = t.border_column
279
+ r.y_paddings = Array.new(all_rows.length)
280
+ r.row_heights = r.default_row_heights
281
+
282
+ all_rows.each_with_index do |row, i|
283
+ r.y_paddings[i] = Array.new(row.length, 0)
284
+ row.each_index do |j|
285
+ column = r.columns[j]
286
+ row_index = has_headers ? i - 1 : i
287
+ st = style(row_index, j)
288
+ mt, mr, mb, ml = st.send(:get_margin)
289
+ pt, pr, pb, pl = st.send(:get_padding)
290
+ column.x_padding = [column.x_padding, ml + mr + pl + pr].max
291
+ column.fixed_width = [column.fixed_width, Gloss::TableRender.style_width(st)].max
292
+ r.row_heights[i] = [r.row_heights[i], Gloss::TableRender.style_height(st)].max
293
+ r.y_paddings[i][j] = mt + mb + pt + pb
294
+ end
295
+ end
296
+
297
+ r.table_width = r.detect_table_width if r.table_width <= 0
298
+
299
+ t.widths, t.heights = r.optimized_widths
300
+ end
301
+
302
+ class << self
303
+ # Style.GetWidth / GetHeight
304
+ def style_width(st) = st.send(:get_width)
305
+ def style_height(st) = st.send(:get_height)
306
+ end
307
+
308
+ class Resizer
309
+ INT32_MAX = 2_147_483_647
310
+
311
+ attr_accessor :table_width, :all_rows, :row_heights, :columns, :wrap, :border_column, :y_paddings
312
+
313
+ def initialize(table_width, headers, all_rows)
314
+ @table_width = table_width
315
+ @headers = headers
316
+ @all_rows = all_rows
317
+ @row_heights = []
318
+ @y_paddings = []
319
+ @columns = []
320
+
321
+ all_rows.each do |row|
322
+ row.each_with_index do |cell, i|
323
+ cell_len = Layout.width(cell)
324
+ if @columns.length <= i
325
+ @columns << Column.new(i, cell_len, cell_len, cell_len, [], 0, 0)
326
+ next
327
+ end
328
+ col = @columns[i]
329
+ col.rows << row
330
+ col.min = cell_len if cell_len < col.min
331
+ col.max = cell_len if cell_len > col.max
332
+ end
333
+ end
334
+ @columns.each_with_index do |col, j|
335
+ col.median = median(col.rows.map { |row| Layout.width(row[j]) })
336
+ end
337
+ end
338
+
339
+ def optimized_widths
340
+ max_total <= @table_width ? expand_table_width : shrink_table_width
341
+ end
342
+
343
+ def detect_table_width
344
+ max_char_count + total_horizontal_padding + total_horizontal_border
345
+ end
346
+
347
+ def expand_table_width
348
+ col_widths = max_column_widths
349
+ loop do
350
+ total = col_widths.sum + total_horizontal_border
351
+ break if total >= @table_width
352
+
353
+ shorter_index = 0
354
+ shorter_width = INT32_MAX
355
+ col_widths.each_with_index do |w, j|
356
+ next if w == @columns[j].fixed_width
357
+
358
+ if w < shorter_width
359
+ shorter_width = w
360
+ shorter_index = j
361
+ end
362
+ end
363
+ col_widths[shorter_index] += 1
364
+ end
365
+ [col_widths, expand_row_heights(col_widths)]
366
+ end
367
+
368
+ def shrink_table_width
369
+ col_widths = max_column_widths
370
+
371
+ shrink_biggest = lambda do |very_big_only|
372
+ loop do
373
+ total = col_widths.sum + total_horizontal_border
374
+ break if total <= @table_width
375
+
376
+ big_index = -INT32_MAX
377
+ big_width = -INT32_MAX
378
+ col_widths.each_with_index do |w, j|
379
+ next if w == @columns[j].fixed_width
380
+
381
+ if very_big_only
382
+ if w >= go_div(@table_width, 2) && w > big_width
383
+ big_width = w
384
+ big_index = j
385
+ end
386
+ elsif w > big_width
387
+ big_width = w
388
+ big_index = j
389
+ end
390
+ end
391
+ break if big_index.negative? || col_widths[big_index].zero?
392
+
393
+ col_widths[big_index] -= 1
394
+ end
395
+ end
396
+
397
+ shrink_to_median = lambda do
398
+ loop do
399
+ total = col_widths.sum + total_horizontal_border
400
+ break if total <= @table_width
401
+
402
+ biggest_diff = -INT32_MAX
403
+ biggest_index = -INT32_MAX
404
+ col_widths.each_with_index do |w, j|
405
+ next if w == @columns[j].fixed_width
406
+
407
+ diff = w - @columns[j].median
408
+ if diff.positive? && diff > biggest_diff
409
+ biggest_diff = diff
410
+ biggest_index = j
411
+ end
412
+ end
413
+ break if biggest_index <= 0 || col_widths[biggest_index].zero?
414
+
415
+ col_widths[biggest_index] -= 1
416
+ end
417
+ end
418
+
419
+ shrink_biggest.call(true)
420
+ shrink_to_median.call
421
+ shrink_biggest.call(false)
422
+
423
+ [col_widths, expand_row_heights(col_widths)]
424
+ end
425
+
426
+ def expand_row_heights(col_widths)
427
+ heights = default_row_heights
428
+ return heights unless @wrap
429
+
430
+ @all_rows.each_with_index do |row, i|
431
+ row.each_with_index do |cell, j|
432
+ h = detect_content_height(cell, col_widths[j] - x_padding_for_col(j)) + x_padding_for_cell(i, j)
433
+ heights[i] = h if h > heights[i]
434
+ end
435
+ end
436
+ heights
437
+ end
438
+
439
+ def default_row_heights
440
+ Array.new(@all_rows.length) do |i|
441
+ h = i < @row_heights.length ? @row_heights[i] : 0
442
+ h < 1 ? 1 : h
443
+ end
444
+ end
445
+
446
+ def max_column_widths
447
+ @columns.map do |col|
448
+ col.fixed_width.positive? ? col.fixed_width : col.max + x_padding_for_col(col.index)
449
+ end
450
+ end
451
+
452
+ def max_char_count
453
+ @columns.sum do |col|
454
+ col.fixed_width.positive? ? col.fixed_width - x_padding_for_col(col.index) : col.max
455
+ end
456
+ end
457
+
458
+ def max_total
459
+ total = 0
460
+ @columns.each_with_index do |col, j|
461
+ total += col.fixed_width.positive? ? col.fixed_width : col.max + x_padding_for_col(j)
462
+ end
463
+ total
464
+ end
465
+
466
+ def total_horizontal_padding = @columns.sum(&:x_padding)
467
+
468
+ def x_padding_for_col(j)
469
+ j >= @columns.length ? 0 : @columns[j].x_padding
470
+ end
471
+
472
+ def x_padding_for_cell(i, j)
473
+ return 0 if i >= @y_paddings.length || @y_paddings[i].nil? || j >= @y_paddings[i].length
474
+
475
+ @y_paddings[i][j]
476
+ end
477
+
478
+ def total_horizontal_border
479
+ (@columns.length * border_per_cell) + extra_border
480
+ end
481
+
482
+ def border_per_cell = @border_column ? 1 : 0
483
+ def extra_border = @border_column ? 1 : 0
484
+
485
+ def detect_content_height(content, width)
486
+ return 1 if width.zero?
487
+
488
+ content = content.gsub("\r\n", "\n")
489
+ content.split("\n", -1).then { |ls| ls.empty? ? [""] : ls }.sum do |line|
490
+ Text.wrap(line, width, "").count("\n") + 1
491
+ end
492
+ end
493
+
494
+ private
495
+
496
+ # util.go median (integer division truncates toward zero; widths are non-negative).
497
+ def median(n)
498
+ n = n.sort
499
+ return 0 if n.empty?
500
+
501
+ if n.length.even?
502
+ h = n.length / 2
503
+ (n[h - 1] + n[h]) / 2
504
+ else
505
+ n[n.length / 2]
506
+ end
507
+ end
508
+
509
+ # Go integer division truncates toward zero.
510
+ def go_div(a, b)
511
+ q = a.abs / b.abs
512
+ (a.negative? ^ b.negative?) ? -q : q
513
+ end
514
+ end
515
+ end
516
+ end
517
+ end
518
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "table/render"
4
+
5
+ # Lipgloss::Table: the lipgloss gem 0.2.2 table (ext/lipgloss/table.c, go/table.go, lib/lipgloss/table.rb;
6
+ # MIT, Marco Roth) over a port of Go lipgloss v1.1.0 table/ (table.go, resizing.go, rows.go, util.go).
7
+ #
8
+ # The gem's Go table is a pointer: every setter mutates it in place and the C extension wraps the same
9
+ # table in a fresh Ruby object of the receiver's class. So `t.row([...])` changes `t`, and the returned
10
+ # object shares that state. Arrays cross into Go as JSON unmarshalled into []string: a non-String element
11
+ # (other than nil, a Symbol or a plain object) fails the unmarshal and the call changes nothing.
12
+ module Lipgloss
13
+ class Table
14
+ Gloss = R2UI::Compat::Gloss
15
+ private_constant :Gloss
16
+
17
+ BORDER_NAMES = %i[normal rounded thick double hidden block outer_half_block inner_half_block ascii
18
+ markdown].freeze
19
+ private_constant :BORDER_NAMES
20
+
21
+ def initialize
22
+ @t = Gloss::TableRender::State.new
23
+ end
24
+
25
+ def headers(headers)
26
+ Gloss::Layout.check_array(headers)
27
+ strs = Gloss::Layout.json_strings(headers)
28
+ @t.headers = strs unless strs.equal?(Gloss::Layout::JSON_FAIL)
29
+ _rewrap
30
+ end
31
+
32
+ def row(row)
33
+ Gloss::Layout.check_array(row)
34
+ strs = Gloss::Layout.json_strings(row)
35
+ @t.append(strs) unless strs.equal?(Gloss::Layout::JSON_FAIL)
36
+ _rewrap
37
+ end
38
+
39
+ def rows(rows)
40
+ Gloss::Layout.check_array(rows)
41
+ parsed = Gloss::TableRender.json_rows(rows)
42
+ parsed&.each { |r| @t.append(r) }
43
+ _rewrap
44
+ end
45
+
46
+ def border(border_sym)
47
+ name = BORDER_NAMES.find { |n| n.equal?(border_sym) } || :normal
48
+ @t.border = Gloss::Borders.fetch(name)
49
+ _rewrap
50
+ end
51
+
52
+ def border_style(style_object)
53
+ Gloss::TableRender.check_style(style_object)
54
+ @t.border_style = style_object
55
+ _rewrap
56
+ end
57
+
58
+ def border_top(value)
59
+ @t.border_top = value ? true : false
60
+ _rewrap
61
+ end
62
+
63
+ def border_bottom(value)
64
+ @t.border_bottom = value ? true : false
65
+ _rewrap
66
+ end
67
+
68
+ def border_left(value)
69
+ @t.border_left = value ? true : false
70
+ _rewrap
71
+ end
72
+
73
+ def border_right(value)
74
+ @t.border_right = value ? true : false
75
+ _rewrap
76
+ end
77
+
78
+ def border_header(value)
79
+ @t.border_header = value ? true : false
80
+ _rewrap
81
+ end
82
+
83
+ def border_column(value)
84
+ @t.border_column = value ? true : false
85
+ _rewrap
86
+ end
87
+
88
+ def border_row(value)
89
+ @t.border_row = value ? true : false
90
+ _rewrap
91
+ end
92
+
93
+ def width(width)
94
+ @t.width = Gloss::Layout.num2int(width)
95
+ _rewrap
96
+ end
97
+
98
+ def height(height)
99
+ @t.height = Gloss::Layout.num2int(height)
100
+ @t.use_manual_height = true
101
+ _rewrap
102
+ end
103
+
104
+ def offset(offset)
105
+ @t.offset = Gloss::Layout.num2int(offset)
106
+ _rewrap
107
+ end
108
+
109
+ def wrap(value)
110
+ @t.wrap = value ? true : false
111
+ _rewrap
112
+ end
113
+
114
+ def clear_rows
115
+ @t.clear_rows
116
+ _rewrap
117
+ end
118
+
119
+ # Apply a pre-computed style map: { "row,col" => style, ... }
120
+ def _style_func_map(style_map)
121
+ unless style_map.is_a?(Hash)
122
+ raise TypeError, "wrong argument type #{Gloss::Layout.type_name(style_map)} (expected Hash)"
123
+ end
124
+
125
+ map = {}
126
+ style_map.each_key do |key|
127
+ style = style_map[key]
128
+ Gloss::TableRender.check_style(style)
129
+ map[key.to_s] = style
130
+ end
131
+ @t.style_map = map
132
+ _rewrap
133
+ end
134
+
135
+ def render
136
+ Gloss::Layout.c_result(Gloss::TableRender.new(@t).render)
137
+ end
138
+
139
+ def to_s
140
+ render
141
+ end
142
+
143
+ private
144
+
145
+ # table_wrap(rb_class_of(self), handle): a new Ruby object over the same table.
146
+ def _rewrap
147
+ obj = self.class.allocate
148
+ obj.instance_variable_set(:@t, @t)
149
+ obj
150
+ end
151
+ end
152
+ end
153
+
154
+ # Ruby-level conveniences, as in the gem's lib/lipgloss/table.rb (MIT, Marco Roth).
155
+ module Lipgloss
156
+ class Table
157
+ # Header row constant (used in style_func)
158
+ HEADER_ROW = -1
159
+
160
+ # Set a style function that determines the style for each cell
161
+ def style_func(rows:, columns:, &block)
162
+ raise ArgumentError, "block required" unless block_given?
163
+ raise ArgumentError, "rows must be >= 0" if rows.negative?
164
+ raise ArgumentError, "columns must be > 0" if columns <= 0
165
+
166
+ style_map = {} #: Hash[String, Style]
167
+
168
+ # Header row
169
+ columns.times do |column|
170
+ style = block.call(HEADER_ROW, column)
171
+ style_map["#{HEADER_ROW},#{column}"] = style if style
172
+ end
173
+
174
+ # Data rows
175
+ rows.times do |row|
176
+ columns.times do |column|
177
+ style = block.call(row, column)
178
+ style_map["#{row},#{column}"] = style if style
179
+ end
180
+ end
181
+
182
+ _style_func_map(style_map)
183
+ end
184
+ end
185
+ end