railbow 0.5.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 +12 -0
- data/lib/railbow/demo/status_demo.rb +5 -3
- data/lib/railbow/status/printer.rb +12 -0
- data/lib/railbow/status/section.rb +15 -4
- data/lib/railbow/table/column.rb +9 -2
- data/lib/railbow/table/renderer.rb +112 -2
- data/lib/railbow/text_utils.rb +30 -0
- data/lib/railbow/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 61c0c3ed9ad2e0c2218e9f2b5e69f6eecec3253a82722408649e91b2822873be
|
|
4
|
+
data.tar.gz: 2179f41a4dfd77f4630940ce0280130a612d75202faede974611c27b0729058d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c777b8cf30ac567ba71c6cc16f16e7f6bbfdc604d8fa1b161cafc17879cea825b446e02f72e43fa9f383d03752c2506a6653c951928302b6d290b689f5a4fa39
|
|
7
|
+
data.tar.gz: 87b8b0d21c779637d0f6c9c20271bec519677feda8d790cadd9644a33413d352550b6a883a96da0774bb7009f9f2667b55d9e23a670800684bbf824b58607b78
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.6.0] - 2026-08-03
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- On a terminal too narrow for the full `db:migrate:status` row, the table no
|
|
13
|
+
longer wraps mid-cell. A width budget degrades it gracefully instead, one
|
|
14
|
+
sacrifice at a time until the rows fit: the Migration Name column narrows
|
|
15
|
+
first (down to 24 columns), then the Tables column is dropped, then the
|
|
16
|
+
formatted date (the raw Migration ID still carries the timestamp), and as a
|
|
17
|
+
last resort the Who column. Each table gives up only as much as its terminal
|
|
18
|
+
demands, and a wide terminal renders exactly as before.
|
|
19
|
+
|
|
8
20
|
## [0.5.0] - 2026-07-28
|
|
9
21
|
|
|
10
22
|
### Added
|
|
@@ -48,15 +48,17 @@ module Railbow
|
|
|
48
48
|
table_columns = [
|
|
49
49
|
Railbow::Table::Column.new(label: "Status", max_width: 6, sticky: true, accent: true),
|
|
50
50
|
Railbow::Table::Column.new(label: "Migration ID", sticky: true),
|
|
51
|
-
Railbow::Table::Column.new(label: "Created At"),
|
|
51
|
+
Railbow::Table::Column.new(label: "Created At", droppable: 2),
|
|
52
52
|
Railbow::Table::Column.new(label: "Migration Name",
|
|
53
53
|
max_width: name_col_width,
|
|
54
|
-
truncate: needs_name_truncation
|
|
54
|
+
truncate: needs_name_truncation,
|
|
55
|
+
shrinkable: true, shrink_floor: 24)
|
|
55
56
|
]
|
|
56
57
|
# No Author column in default config (author:me only highlights)
|
|
57
58
|
|
|
58
59
|
tables_truncate_fn = ->(cell_raw, max_w) { formatter.table_tags_fitted(cell_raw, max_w) }
|
|
59
|
-
table_columns << Railbow::Table::Column.new(label: "Tables",
|
|
60
|
+
table_columns << Railbow::Table::Column.new(label: "Tables", droppable: 1,
|
|
61
|
+
truncate: true, truncate_fn: tables_truncate_fn)
|
|
60
62
|
|
|
61
63
|
# Build rows
|
|
62
64
|
highlight_rows = Set.new
|
|
@@ -263,10 +263,22 @@ module Railbow
|
|
|
263
263
|
head += "#{RULE * 4} #{shown_count(section)} of #{section.total_count} " if section.hidden_count > 0
|
|
264
264
|
|
|
265
265
|
width = (rule_width > 0) ? rule_width : DEFAULT_RULE_WIDTH
|
|
266
|
+
if (term = terminal_width)
|
|
267
|
+
width = [width, term].min
|
|
268
|
+
end
|
|
266
269
|
filler = [width - formatter.display_width(head), 4].max
|
|
267
270
|
"#{PURPLE}#{head}#{RULE * filler}#{RESET}"
|
|
268
271
|
end
|
|
269
272
|
|
|
273
|
+
# The rule frames the table, but on a narrow terminal the table itself
|
|
274
|
+
# gives up width, so the rule must never trust fixed_width alone.
|
|
275
|
+
def terminal_width
|
|
276
|
+
return $stdout.winsize[1] if $stdout.respond_to?(:winsize) && $stdout.tty?
|
|
277
|
+
nil
|
|
278
|
+
rescue
|
|
279
|
+
nil
|
|
280
|
+
end
|
|
281
|
+
|
|
270
282
|
# A bare "10 of 50" reads as "10 fell inside the window". When the floor
|
|
271
283
|
# overrode the window the selection is a tail rather than a window, so it
|
|
272
284
|
# says "last 10 of 50" - the same words the single-database note uses.
|
|
@@ -30,6 +30,10 @@ module Railbow
|
|
|
30
30
|
# with it for horizontal space (tags, authors, table names).
|
|
31
31
|
NAME_COL_WIDTH = 60
|
|
32
32
|
|
|
33
|
+
# Floor the width budget may shrink the name column down to on a narrow
|
|
34
|
+
# terminal, before it starts dropping columns instead.
|
|
35
|
+
NAME_COL_MIN_WIDTH = 24
|
|
36
|
+
|
|
33
37
|
NO_FILE = "NO FILE"
|
|
34
38
|
|
|
35
39
|
# A version one database in a shard group has and another does not.
|
|
@@ -311,6 +315,11 @@ module Railbow
|
|
|
311
315
|
|
|
312
316
|
# The name column only needs capping when something else competes for the
|
|
313
317
|
# row: table tags, an author column, branch badges or landed badges.
|
|
318
|
+
#
|
|
319
|
+
# On a terminal too narrow for the full row the width budget degrades the
|
|
320
|
+
# table instead of letting it wrap: the name column shrinks first, then
|
|
321
|
+
# Tables is dropped, then the formatted date (the raw Migration ID keeps
|
|
322
|
+
# the timestamp), then Who.
|
|
314
323
|
def build_columns
|
|
315
324
|
@name_col_width = needs_name_truncation? ? NAME_COL_WIDTH : nil
|
|
316
325
|
|
|
@@ -318,15 +327,17 @@ module Railbow
|
|
|
318
327
|
Table::Column.new(label: "Status", max_width: status_col_width,
|
|
319
328
|
sticky: true, accent: true, aliased: !sharded?),
|
|
320
329
|
Table::Column.new(label: "Migration ID", sticky: true),
|
|
321
|
-
Table::Column.new(label: (date_format == "full") ? "Created At" : "Date"
|
|
330
|
+
Table::Column.new(label: (date_format == "full") ? "Created At" : "Date",
|
|
331
|
+
droppable: 2),
|
|
322
332
|
Table::Column.new(label: "Migration Name",
|
|
323
333
|
max_width: @name_col_width,
|
|
324
|
-
truncate: !@name_col_width.nil
|
|
334
|
+
truncate: !@name_col_width.nil?,
|
|
335
|
+
shrinkable: true, shrink_floor: NAME_COL_MIN_WIDTH)
|
|
325
336
|
]
|
|
326
|
-
cols << Table::Column.new(label: "Who") if author_mode == "all"
|
|
337
|
+
cols << Table::Column.new(label: "Who", droppable: 3) if author_mode == "all"
|
|
327
338
|
if tables_enabled?
|
|
328
339
|
truncate_fn = ->(cell_raw, max_w) { formatter.table_tags_fitted(cell_raw, max_w) }
|
|
329
|
-
cols << Table::Column.new(label: "Tables",
|
|
340
|
+
cols << Table::Column.new(label: "Tables", droppable: 1,
|
|
330
341
|
truncate: Railbow::Params.compact_oneline?, truncate_fn: truncate_fn)
|
|
331
342
|
end
|
|
332
343
|
cols
|
data/lib/railbow/table/column.rb
CHANGED
|
@@ -4,14 +4,18 @@ module Railbow
|
|
|
4
4
|
module Table
|
|
5
5
|
class Column
|
|
6
6
|
attr_reader :label, :width, :min_width, :max_width, :align, :truncate, :truncate_fn,
|
|
7
|
-
:sticky, :accent, :aliased
|
|
7
|
+
:sticky, :accent, :aliased, :shrinkable, :shrink_floor, :droppable
|
|
8
8
|
|
|
9
9
|
# accent: the column carries its own meaning through color (a status
|
|
10
10
|
# glyph, say), so it keeps that color when the row is dimmed.
|
|
11
11
|
# aliased: value aliases from config may rewrite this column's cells.
|
|
12
12
|
# Off for cells the caller already resolved, such as a cluster of status
|
|
13
13
|
# glyphs, which no whole-cell alias could ever match.
|
|
14
|
-
|
|
14
|
+
# shrinkable: the width budget may narrow this column, down to
|
|
15
|
+
# shrink_floor, before it starts dropping columns.
|
|
16
|
+
# droppable: rank in the order the width budget drops columns entirely
|
|
17
|
+
# when shrinking is not enough - lower ranks go first. Nil: never dropped.
|
|
18
|
+
def initialize(label:, width: :auto, min_width: nil, max_width: nil, align: :left, truncate: false, truncate_fn: nil, sticky: false, accent: false, aliased: true, shrinkable: false, shrink_floor: nil, droppable: nil)
|
|
15
19
|
@label = label
|
|
16
20
|
@width = width
|
|
17
21
|
@min_width = min_width
|
|
@@ -22,6 +26,9 @@ module Railbow
|
|
|
22
26
|
@sticky = sticky
|
|
23
27
|
@accent = accent
|
|
24
28
|
@aliased = aliased
|
|
29
|
+
@shrinkable = shrinkable
|
|
30
|
+
@shrink_floor = shrink_floor
|
|
31
|
+
@droppable = droppable
|
|
25
32
|
end
|
|
26
33
|
|
|
27
34
|
def fixed?
|
|
@@ -14,15 +14,25 @@ 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
|
|
20
25
|
# given value, which is how several tables rendered in one run line their
|
|
21
26
|
# columns up with each other. An explicit compact maxw still wins.
|
|
22
|
-
|
|
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)
|
|
23
32
|
@compact = compact
|
|
24
33
|
@aliases = aliases
|
|
25
34
|
@min_widths = min_widths
|
|
35
|
+
@term_width = term_width
|
|
26
36
|
@reverse_col_aliases = aliases[:columns]&.invert || {}
|
|
27
37
|
@columns = apply_hidden_columns(columns)
|
|
28
38
|
@theme = theme
|
|
@@ -50,7 +60,21 @@ module Railbow
|
|
|
50
60
|
return "" if columns.empty?
|
|
51
61
|
|
|
52
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
|
|
75
|
+
|
|
53
76
|
resolved = resolve_widths(rows)
|
|
77
|
+
shrink_to_fit!(resolved, rows)
|
|
54
78
|
|
|
55
79
|
# In oneline mode, truncate non-sticky non-last columns at resolved width
|
|
56
80
|
if @compact[:oneline]
|
|
@@ -209,7 +233,7 @@ module Railbow
|
|
|
209
233
|
last_cell_plain = strip_ansi(last_cell_raw)
|
|
210
234
|
term_w = terminal_width
|
|
211
235
|
prefix_width = compute_prefix_width(widths, last)
|
|
212
|
-
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
|
|
213
237
|
|
|
214
238
|
# Use custom truncate_fn if available (e.g. table tags with +N)
|
|
215
239
|
if columns[last].truncate_fn && last_col_max &&
|
|
@@ -305,12 +329,98 @@ module Railbow
|
|
|
305
329
|
end
|
|
306
330
|
|
|
307
331
|
def terminal_width
|
|
332
|
+
return @term_width if @term_width
|
|
308
333
|
return $stdout.winsize[1] if $stdout.respond_to?(:winsize) && $stdout.tty?
|
|
309
334
|
nil
|
|
310
335
|
rescue
|
|
311
336
|
nil
|
|
312
337
|
end
|
|
313
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
|
+
|
|
314
424
|
def ansi_word_wrap(str, max_width)
|
|
315
425
|
plain = strip_ansi(str)
|
|
316
426
|
plain_lines = word_wrap(plain, max_width)
|
data/lib/railbow/text_utils.rb
CHANGED
|
@@ -26,5 +26,35 @@ module Railbow
|
|
|
26
26
|
end
|
|
27
27
|
"#{truncated}..."
|
|
28
28
|
end
|
|
29
|
+
|
|
30
|
+
# ANSI-aware counterpart to truncate_str: escape codes pass through
|
|
31
|
+
# unmeasured, and a color left open at the cut is closed before the
|
|
32
|
+
# ellipsis so it cannot leak into whatever follows the cell.
|
|
33
|
+
def truncate_ansi(str, max_width)
|
|
34
|
+
s = str.to_s
|
|
35
|
+
return s if display_width(strip_ansi(s)) <= max_width
|
|
36
|
+
|
|
37
|
+
result = +""
|
|
38
|
+
width = 0
|
|
39
|
+
open_color = false
|
|
40
|
+
i = 0
|
|
41
|
+
while i < s.length
|
|
42
|
+
if s[i] == "\e" && (close = s.index("m", i))
|
|
43
|
+
code = s[i..close]
|
|
44
|
+
result << code
|
|
45
|
+
open_color = (code != "\e[0m")
|
|
46
|
+
i = close + 1
|
|
47
|
+
else
|
|
48
|
+
ch = s[i]
|
|
49
|
+
ch_width = display_width(ch)
|
|
50
|
+
break if width + ch_width > max_width - 3
|
|
51
|
+
result << ch
|
|
52
|
+
width += ch_width
|
|
53
|
+
i += 1
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
result << "\e[0m" if open_color
|
|
57
|
+
"#{result}..."
|
|
58
|
+
end
|
|
29
59
|
end
|
|
30
60
|
end
|
data/lib/railbow/version.rb
CHANGED