railbow 0.4.0 → 0.6.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 +66 -0
- data/README.md +79 -1
- data/lib/railbow/config.rb +3 -1
- data/lib/railbow/demo/status_demo.rb +5 -3
- data/lib/railbow/init.rb +16 -0
- data/lib/railbow/multi_db.rb +87 -0
- data/lib/railbow/params.rb +54 -0
- data/lib/railbow/status/ghosts.rb +144 -0
- data/lib/railbow/status/git_data.rb +327 -0
- data/lib/railbow/status/help.rb +121 -0
- data/lib/railbow/status/printer.rb +336 -0
- data/lib/railbow/status/section.rb +562 -0
- data/lib/railbow/table/column.rb +14 -2
- data/lib/railbow/table/renderer.rb +158 -19
- data/lib/railbow/tasks/migrate_status.rake +30 -756
- data/lib/railbow/text_utils.rb +30 -0
- data/lib/railbow/version.rb +1 -1
- metadata +7 -1
|
@@ -14,38 +14,67 @@ module Railbow
|
|
|
14
14
|
GHOST_FG = "\e[38;5;217m" # warm pink foreground for contrast
|
|
15
15
|
DIMMED_FG = "\e[38;5;242m" # muted grey - a row that is not currently in effect
|
|
16
16
|
|
|
17
|
+
# The flexing last column never gets less than this from the terminal.
|
|
18
|
+
MIN_LAST_COL = 10
|
|
19
|
+
# Default floor for shrinkable columns that name no shrink_floor.
|
|
20
|
+
MIN_SHRINK_WIDTH = 16
|
|
21
|
+
|
|
17
22
|
attr_reader :columns, :theme
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
# min_widths raises the resolved width of each column to at least the
|
|
25
|
+
# given value, which is how several tables rendered in one run line their
|
|
26
|
+
# columns up with each other. An explicit compact maxw still wins.
|
|
27
|
+
#
|
|
28
|
+
# term_width overrides the detected terminal width; render re-invokes
|
|
29
|
+
# itself through a reduced renderer when the budget drops a column, and
|
|
30
|
+
# the width it fits must be the same one this instance measured.
|
|
31
|
+
def initialize(columns:, theme:, compact: {}, aliases: {}, min_widths: nil, term_width: nil)
|
|
20
32
|
@compact = compact
|
|
21
33
|
@aliases = aliases
|
|
34
|
+
@min_widths = min_widths
|
|
35
|
+
@term_width = term_width
|
|
22
36
|
@reverse_col_aliases = aliases[:columns]&.invert || {}
|
|
23
37
|
@columns = apply_hidden_columns(columns)
|
|
24
38
|
@theme = theme
|
|
25
39
|
end
|
|
26
40
|
|
|
27
|
-
|
|
28
|
-
|
|
41
|
+
# The widths this table would resolve to on its own. Callers rendering
|
|
42
|
+
# several tables together take the per-column maximum and feed it back as
|
|
43
|
+
# min_widths.
|
|
44
|
+
def column_widths(rows)
|
|
45
|
+
return [] if columns.empty?
|
|
29
46
|
|
|
30
|
-
|
|
31
|
-
|
|
47
|
+
resolve_widths(prepare_rows(rows))
|
|
48
|
+
end
|
|
32
49
|
|
|
33
|
-
|
|
34
|
-
|
|
50
|
+
# Width of everything left of the last column. The last column flexes to
|
|
51
|
+
# the terminal, so this is where furniture drawn around the table (a
|
|
52
|
+
# section rule, say) can stop without running past it.
|
|
53
|
+
def fixed_width(widths)
|
|
54
|
+
return 0 if columns.empty? || widths.nil? || widths.empty?
|
|
35
55
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
56
|
+
compute_prefix_width(widths, columns.size - 1)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def render(rows, separators: {}, highlight_rows: Set.new, ghost_rows: Set.new, dim_rows: Set.new, tick_rows: Set.new, tick_col: nil)
|
|
60
|
+
return "" if columns.empty?
|
|
61
|
+
|
|
62
|
+
rows = prepare_rows(rows)
|
|
63
|
+
|
|
64
|
+
# Width budget: when even fully shrunk columns cannot bring the table
|
|
65
|
+
# under the terminal width, sacrifice the most expendable column and
|
|
66
|
+
# render without it. One column per pass - the reduced renderer asks
|
|
67
|
+
# again with what is left.
|
|
68
|
+
if (drop = drop_index_to_fit(rows))
|
|
69
|
+
return dropped_renderer(drop).render(
|
|
70
|
+
rows.map { |row| row.reject.with_index { |_, i| i == drop } },
|
|
71
|
+
separators: separators, highlight_rows: highlight_rows, ghost_rows: ghost_rows,
|
|
72
|
+
dim_rows: dim_rows, tick_rows: tick_rows, tick_col: shift_tick_col(tick_col, drop)
|
|
73
|
+
)
|
|
74
|
+
end
|
|
47
75
|
|
|
48
76
|
resolved = resolve_widths(rows)
|
|
77
|
+
shrink_to_fit!(resolved, rows)
|
|
49
78
|
|
|
50
79
|
# In oneline mode, truncate non-sticky non-last columns at resolved width
|
|
51
80
|
if @compact[:oneline]
|
|
@@ -77,6 +106,25 @@ module Railbow
|
|
|
77
106
|
|
|
78
107
|
private
|
|
79
108
|
|
|
109
|
+
# Everything that changes a cell's content, and therefore its width,
|
|
110
|
+
# before widths are resolved: hidden columns, value aliases, and the
|
|
111
|
+
# pre-truncation of non-last columns that cap themselves.
|
|
112
|
+
def prepare_rows(rows)
|
|
113
|
+
rows = remap_rows(rows) if @hidden_indices&.any?
|
|
114
|
+
rows = apply_value_aliases(rows) if @aliases[:values]&.any?
|
|
115
|
+
|
|
116
|
+
rows.map { |row|
|
|
117
|
+
row.each_with_index.map { |cell, i|
|
|
118
|
+
col = columns[i]
|
|
119
|
+
if col&.truncate && col.max_width && i < columns.size - 1
|
|
120
|
+
truncate_str(cell.to_s, col.max_width)
|
|
121
|
+
else
|
|
122
|
+
cell
|
|
123
|
+
end
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
|
|
80
128
|
def resolve_widths(rows)
|
|
81
129
|
all_rows = rows
|
|
82
130
|
last = columns.size - 1
|
|
@@ -95,6 +143,9 @@ module Railbow
|
|
|
95
143
|
w = [w, col.min_width].max if col.min_width
|
|
96
144
|
w = [w, col.max_width].min if col.max_width
|
|
97
145
|
end
|
|
146
|
+
# Alignment across tables raises the width; an explicit maxw caps it
|
|
147
|
+
# afterwards, so the user's cap stays authoritative.
|
|
148
|
+
w = [w, @min_widths[i].to_i].max if @min_widths && i != last && w > 0
|
|
98
149
|
w = [w, global_maxw].min if global_maxw && i != last && w > 0
|
|
99
150
|
w
|
|
100
151
|
end
|
|
@@ -182,7 +233,7 @@ module Railbow
|
|
|
182
233
|
last_cell_plain = strip_ansi(last_cell_raw)
|
|
183
234
|
term_w = terminal_width
|
|
184
235
|
prefix_width = compute_prefix_width(widths, last)
|
|
185
|
-
last_col_max = term_w ? [term_w - prefix_width - display_width(pad),
|
|
236
|
+
last_col_max = term_w ? [term_w - prefix_width - display_width(pad), MIN_LAST_COL].max : nil
|
|
186
237
|
|
|
187
238
|
# Use custom truncate_fn if available (e.g. table tags with +N)
|
|
188
239
|
if columns[last].truncate_fn && last_col_max &&
|
|
@@ -278,12 +329,98 @@ module Railbow
|
|
|
278
329
|
end
|
|
279
330
|
|
|
280
331
|
def terminal_width
|
|
332
|
+
return @term_width if @term_width
|
|
281
333
|
return $stdout.winsize[1] if $stdout.respond_to?(:winsize) && $stdout.tty?
|
|
282
334
|
nil
|
|
283
335
|
rescue
|
|
284
336
|
nil
|
|
285
337
|
end
|
|
286
338
|
|
|
339
|
+
# --- Width budget ---
|
|
340
|
+
|
|
341
|
+
# The index of the column to sacrifice, or nil while shrinking alone can
|
|
342
|
+
# still fit the table into the terminal.
|
|
343
|
+
def drop_index_to_fit(rows)
|
|
344
|
+
return nil unless terminal_width
|
|
345
|
+
return nil if rows.empty?
|
|
346
|
+
|
|
347
|
+
candidate = columns.each_index
|
|
348
|
+
.select { |i| columns[i].droppable }
|
|
349
|
+
.min_by { |i| columns[i].droppable }
|
|
350
|
+
return nil unless candidate
|
|
351
|
+
return nil if required_width(fully_shrunk_widths(rows), rows) <= terminal_width
|
|
352
|
+
|
|
353
|
+
candidate
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def dropped_renderer(drop)
|
|
357
|
+
self.class.new(
|
|
358
|
+
columns: columns.reject.with_index { |_, i| i == drop },
|
|
359
|
+
theme: theme,
|
|
360
|
+
compact: @compact,
|
|
361
|
+
aliases: @aliases,
|
|
362
|
+
min_widths: @min_widths&.reject&.with_index { |_, i| i == drop },
|
|
363
|
+
term_width: terminal_width
|
|
364
|
+
)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def shift_tick_col(tick_col, drop)
|
|
368
|
+
return nil if tick_col.nil? || drop == tick_col
|
|
369
|
+
|
|
370
|
+
(drop < tick_col) ? tick_col - 1 : tick_col
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Narrows shrinkable columns just enough to close the overflow, and
|
|
374
|
+
# re-truncates their cells to the new width.
|
|
375
|
+
def shrink_to_fit!(widths, rows)
|
|
376
|
+
return if rows.empty? || !terminal_width
|
|
377
|
+
|
|
378
|
+
overflow = required_width(widths, rows) - terminal_width
|
|
379
|
+
return if overflow <= 0
|
|
380
|
+
|
|
381
|
+
last = columns.size - 1
|
|
382
|
+
columns.each_with_index do |col, i|
|
|
383
|
+
break if overflow <= 0
|
|
384
|
+
next if i == last || !col.shrinkable
|
|
385
|
+
|
|
386
|
+
cut = [widths[i] - shrink_floor(col), overflow].min
|
|
387
|
+
next if cut <= 0
|
|
388
|
+
|
|
389
|
+
widths[i] -= cut
|
|
390
|
+
overflow -= cut
|
|
391
|
+
rows.each { |row| row[i] = truncate_ansi(row[i].to_s, widths[i]) }
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# Never below the header label: a column narrower than its own header
|
|
396
|
+
# would push the header row out of alignment.
|
|
397
|
+
def shrink_floor(col)
|
|
398
|
+
[col.shrink_floor || MIN_SHRINK_WIDTH, display_width(effective_label(col))].max
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def fully_shrunk_widths(rows)
|
|
402
|
+
widths = resolve_widths(rows)
|
|
403
|
+
last = columns.size - 1
|
|
404
|
+
columns.each_with_index do |col, i|
|
|
405
|
+
next if i == last || !col.shrinkable
|
|
406
|
+
|
|
407
|
+
widths[i] = [widths[i], shrink_floor(col)].min
|
|
408
|
+
end
|
|
409
|
+
widths
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# What the table needs from the terminal: every fixed column plus a
|
|
413
|
+
# reserve for the flexing last column, which truncates down to
|
|
414
|
+
# MIN_LAST_COL but never below it. The last column's header label counts
|
|
415
|
+
# too - it is drawn as-is, so a label wider than every cell would poke
|
|
416
|
+
# past the terminal edge otherwise.
|
|
417
|
+
def required_width(widths, rows)
|
|
418
|
+
last = columns.size - 1
|
|
419
|
+
last_w = rows.map { |row| display_width(strip_ansi(row[last].to_s)) }.max || 0
|
|
420
|
+
last_w = [last_w, display_width(effective_label(columns[last]))].max unless @compact[:noheader]
|
|
421
|
+
compute_prefix_width(widths, last) + [last_w, MIN_LAST_COL].min + display_width(effective_padding)
|
|
422
|
+
end
|
|
423
|
+
|
|
287
424
|
def ansi_word_wrap(str, max_width)
|
|
288
425
|
plain = strip_ansi(str)
|
|
289
426
|
plain_lines = word_wrap(plain, max_width)
|
|
@@ -429,6 +566,8 @@ module Railbow
|
|
|
429
566
|
# Build column index → value alias map
|
|
430
567
|
col_map = {}
|
|
431
568
|
columns.each_with_index do |col, i|
|
|
569
|
+
next unless col.aliased
|
|
570
|
+
|
|
432
571
|
label = col.label
|
|
433
572
|
col_map[i] = value_aliases[label] if value_aliases[label]
|
|
434
573
|
# Also look up by original name if column was renamed by alias
|