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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +64 -0
- data/README.md +79 -0
- data/lib/r2ui/app.rb +311 -34
- data/lib/r2ui/canvas.rb +135 -14
- data/lib/r2ui/cli/builder.rb +72 -0
- data/lib/r2ui/cli/context.rb +45 -0
- data/lib/r2ui/cli/definition.rb +174 -0
- data/lib/r2ui/cli/ext/ask.rb +133 -0
- data/lib/r2ui/cli/ext/box.rb +112 -0
- data/lib/r2ui/cli/ext/choose.rb +162 -0
- data/lib/r2ui/cli/ext/choose_many.rb +241 -0
- data/lib/r2ui/cli/ext/color_option.rb +64 -0
- data/lib/r2ui/cli/ext/completion.rb +520 -0
- data/lib/r2ui/cli/ext/config.rb +179 -0
- data/lib/r2ui/cli/ext/confirm.rb +110 -0
- data/lib/r2ui/cli/ext/dashboard.rb +105 -0
- data/lib/r2ui/cli/ext/diff.rb +195 -0
- data/lib/r2ui/cli/ext/editor.rb +94 -0
- data/lib/r2ui/cli/ext/env.rb +108 -0
- data/lib/r2ui/cli/ext/examples.rb +62 -0
- data/lib/r2ui/cli/ext/filter.rb +269 -0
- data/lib/r2ui/cli/ext/format.rb +104 -0
- data/lib/r2ui/cli/ext/list.rb +117 -0
- data/lib/r2ui/cli/ext/log.rb +89 -0
- data/lib/r2ui/cli/ext/pager.rb +97 -0
- data/lib/r2ui/cli/ext/pairs.rb +62 -0
- data/lib/r2ui/cli/ext/parallel.rb +274 -0
- data/lib/r2ui/cli/ext/password.rb +158 -0
- data/lib/r2ui/cli/ext/progress.rb +330 -0
- data/lib/r2ui/cli/ext/reference.rb +129 -0
- data/lib/r2ui/cli/ext/spinner.rb +165 -0
- data/lib/r2ui/cli/ext/summary.rb +63 -0
- data/lib/r2ui/cli/ext/table.rb +141 -0
- data/lib/r2ui/cli/ext/tasks.rb +216 -0
- data/lib/r2ui/cli/ext/trace.rb +64 -0
- data/lib/r2ui/cli/ext/tree.rb +114 -0
- data/lib/r2ui/cli/ext/version.rb +67 -0
- data/lib/r2ui/cli/extension.rb +129 -0
- data/lib/r2ui/cli/help.rb +115 -0
- data/lib/r2ui/cli/helpers.rb +20 -0
- data/lib/r2ui/cli/live.rb +150 -0
- data/lib/r2ui/cli/parser.rb +165 -0
- data/lib/r2ui/cli/prompt.rb +133 -0
- data/lib/r2ui/cli/runner.rb +86 -0
- data/lib/r2ui/cli/shell.rb +185 -0
- data/lib/r2ui/cli/testing.rb +49 -0
- data/lib/r2ui/cli.rb +129 -0
- data/lib/r2ui/compat/bubbletea/LICENSE-bubbletea.txt +21 -0
- data/lib/r2ui/compat/bubbletea/ansi.rb +424 -0
- data/lib/r2ui/compat/bubbletea/commands.rb +143 -0
- data/lib/r2ui/compat/bubbletea/input.rb +269 -0
- data/lib/r2ui/compat/bubbletea/input_reader.rb +66 -0
- data/lib/r2ui/compat/bubbletea/messages.rb +251 -0
- data/lib/r2ui/compat/bubbletea/model.rb +65 -0
- data/lib/r2ui/compat/bubbletea/native.rb +254 -0
- data/lib/r2ui/compat/bubbletea/renderer.rb +192 -0
- data/lib/r2ui/compat/bubbletea/runner.rb +425 -0
- data/lib/r2ui/compat/bubbletea/terminal.rb +230 -0
- data/lib/r2ui/compat/bubbletea/version.rb +8 -0
- data/lib/r2ui/compat/bubbletea/width_table.rb +228 -0
- data/lib/r2ui/compat/bubbletea.rb +16 -0
- data/lib/r2ui/compat/lipgloss/border.rb +17 -0
- data/lib/r2ui/compat/lipgloss/color.rb +78 -0
- data/lib/r2ui/compat/lipgloss/color_blend.rb +166 -0
- data/lib/r2ui/compat/lipgloss/colorful.rb +585 -0
- data/lib/r2ui/compat/lipgloss/layout.rb +461 -0
- data/lib/r2ui/compat/lipgloss/list.rb +85 -0
- data/lib/r2ui/compat/lipgloss/position.rb +30 -0
- data/lib/r2ui/compat/lipgloss/style/border.rb +134 -0
- data/lib/r2ui/compat/lipgloss/style.rb +1062 -0
- data/lib/r2ui/compat/lipgloss/table/render.rb +518 -0
- data/lib/r2ui/compat/lipgloss/table.rb +185 -0
- data/lib/r2ui/compat/lipgloss/termenv.rb +768 -0
- data/lib/r2ui/compat/lipgloss/text/unicode_tables.rb +428 -0
- data/lib/r2ui/compat/lipgloss/text.rb +1555 -0
- data/lib/r2ui/compat/lipgloss/tree/go_tree.rb +243 -0
- data/lib/r2ui/compat/lipgloss/tree.rb +105 -0
- data/lib/r2ui/compat/lipgloss/version.rb +15 -0
- data/lib/r2ui/compat/lipgloss.rb +64 -0
- data/lib/r2ui/compat/load_path/bubbletea.rb +4 -0
- data/lib/r2ui/compat/load_path/lipgloss.rb +4 -0
- data/lib/r2ui/component.rb +67 -0
- data/lib/r2ui/context.rb +87 -0
- data/lib/r2ui/drop_in.rb +16 -0
- data/lib/r2ui/dsl/column.rb +11 -3
- data/lib/r2ui/dsl/dashboard.rb +45 -9
- data/lib/r2ui/dsl/resource.rb +12 -0
- data/lib/r2ui/ext/after.rb +54 -0
- data/lib/r2ui/ext/async.rb +81 -0
- data/lib/r2ui/ext/batch_sequence.rb +54 -0
- data/lib/r2ui/ext/countdown.rb +52 -0
- data/lib/r2ui/ext/data_table.rb +71 -0
- data/lib/r2ui/ext/every.rb +66 -0
- data/lib/r2ui/ext/exec.rb +116 -0
- data/lib/r2ui/ext/file_picker.rb +78 -0
- data/lib/r2ui/ext/focus_report.rb +69 -0
- data/lib/r2ui/ext/help.rb +62 -0
- data/lib/r2ui/ext/keys.rb +64 -0
- data/lib/r2ui/ext/list.rb +145 -0
- data/lib/r2ui/ext/messages.rb +87 -0
- data/lib/r2ui/ext/mouse.rb +134 -0
- data/lib/r2ui/ext/paginator.rb +86 -0
- data/lib/r2ui/ext/paste.rb +86 -0
- data/lib/r2ui/ext/println.rb +21 -0
- data/lib/r2ui/ext/progress.rb +85 -0
- data/lib/r2ui/ext/refresh.rb +56 -0
- data/lib/r2ui/ext/resize.rb +62 -0
- data/lib/r2ui/ext/screen.rb +47 -0
- data/lib/r2ui/ext/screen_mode.rb +63 -0
- data/lib/r2ui/ext/spinner.rb +59 -0
- data/lib/r2ui/ext/state.rb +45 -0
- data/lib/r2ui/ext/stopwatch.rb +67 -0
- data/lib/r2ui/ext/suspend.rb +56 -0
- data/lib/r2ui/ext/text_area.rb +73 -0
- data/lib/r2ui/ext/text_input.rb +83 -0
- data/lib/r2ui/ext/theme.rb +112 -0
- data/lib/r2ui/ext/view.rb +37 -0
- data/lib/r2ui/ext/viewport.rb +92 -0
- data/lib/r2ui/ext/window_title.rb +51 -0
- data/lib/r2ui/extension.rb +199 -0
- data/lib/r2ui/history.rb +4 -1
- data/lib/r2ui/keys.rb +35 -8
- data/lib/r2ui/motion.rb +169 -0
- data/lib/r2ui/panel_state.rb +46 -2
- data/lib/r2ui/renderer.rb +87 -22
- data/lib/r2ui/version.rb +1 -1
- data/lib/r2ui/widgets/box.rb +6 -3
- data/lib/r2ui/widgets/glyphs.rb +149 -0
- data/lib/r2ui/widgets/table.rb +109 -25
- data/lib/r2ui.rb +12 -1
- metadata +119 -4
- 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
|