railbow 0.3.0 → 0.5.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 +95 -0
- data/README.md +121 -3
- data/lib/railbow/calendar.rb +89 -0
- data/lib/railbow/config.rb +3 -1
- data/lib/railbow/demo/status_demo.rb +22 -38
- data/lib/railbow/init.rb +23 -1
- data/lib/railbow/multi_db.rb +87 -0
- data/lib/railbow/params.rb +74 -1
- 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 +324 -0
- data/lib/railbow/status/section.rb +551 -0
- data/lib/railbow/table/column.rb +10 -2
- data/lib/railbow/table/renderer.rb +106 -48
- data/lib/railbow/table/theme.rb +3 -2
- data/lib/railbow/tasks/migrate_status.rake +30 -765
- data/lib/railbow/version.rb +1 -1
- metadata +8 -1
|
@@ -10,40 +10,46 @@ module Railbow
|
|
|
10
10
|
|
|
11
11
|
RESET = "\e[0m"
|
|
12
12
|
WHITE = "\e[97m"
|
|
13
|
-
GHOST_BG = "\e[48;5;52m" # deep red/maroon background
|
|
13
|
+
GHOST_BG = "\e[48;5;52m" # deep red/maroon background - stands out as abnormal
|
|
14
14
|
GHOST_FG = "\e[38;5;217m" # warm pink foreground for contrast
|
|
15
|
+
DIMMED_FG = "\e[38;5;242m" # muted grey - a row that is not currently in effect
|
|
15
16
|
|
|
16
17
|
attr_reader :columns, :theme
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
# min_widths raises the resolved width of each column to at least the
|
|
20
|
+
# given value, which is how several tables rendered in one run line their
|
|
21
|
+
# columns up with each other. An explicit compact maxw still wins.
|
|
22
|
+
def initialize(columns:, theme:, compact: {}, aliases: {}, min_widths: nil)
|
|
19
23
|
@compact = compact
|
|
20
24
|
@aliases = aliases
|
|
25
|
+
@min_widths = min_widths
|
|
21
26
|
@reverse_col_aliases = aliases[:columns]&.invert || {}
|
|
22
27
|
@columns = apply_hidden_columns(columns)
|
|
23
28
|
@theme = theme
|
|
24
29
|
end
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
# The widths this table would resolve to on its own. Callers rendering
|
|
32
|
+
# several tables together take the per-column maximum and feed it back as
|
|
33
|
+
# min_widths.
|
|
34
|
+
def column_widths(rows)
|
|
35
|
+
return [] if columns.empty?
|
|
28
36
|
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
resolve_widths(prepare_rows(rows))
|
|
38
|
+
end
|
|
31
39
|
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
# Width of everything left of the last column. The last column flexes to
|
|
41
|
+
# the terminal, so this is where furniture drawn around the table (a
|
|
42
|
+
# section rule, say) can stop without running past it.
|
|
43
|
+
def fixed_width(widths)
|
|
44
|
+
return 0 if columns.empty? || widths.nil? || widths.empty?
|
|
34
45
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
truncate_str(cell.to_s, col.max_width)
|
|
41
|
-
else
|
|
42
|
-
cell
|
|
43
|
-
end
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
+
compute_prefix_width(widths, columns.size - 1)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def render(rows, separators: {}, highlight_rows: Set.new, ghost_rows: Set.new, dim_rows: Set.new, tick_rows: Set.new, tick_col: nil)
|
|
50
|
+
return "" if columns.empty?
|
|
46
51
|
|
|
52
|
+
rows = prepare_rows(rows)
|
|
47
53
|
resolved = resolve_widths(rows)
|
|
48
54
|
|
|
49
55
|
# In oneline mode, truncate non-sticky non-last columns at resolved width
|
|
@@ -64,12 +70,11 @@ module Railbow
|
|
|
64
70
|
rows.each_with_index do |row, i|
|
|
65
71
|
tc = (tick_rows.include?(i) && tick_col) ? tick_col : nil
|
|
66
72
|
if separators.key?(i) && theme.format_separator
|
|
67
|
-
|
|
68
|
-
sep_row[1] = theme.format_separator.call(separators[i]) if columns.size > 1
|
|
69
|
-
lines << render_row(sep_row, resolved, tick_col: tc, tick_cross: true)
|
|
73
|
+
lines << render_separator_row(separators[i], resolved, tick_col: tc)
|
|
70
74
|
tc = nil # tick already shown on separator row
|
|
71
75
|
end
|
|
72
|
-
formatted = render_row(row, resolved, tick_col: tc, highlight: highlight_rows.include?(i),
|
|
76
|
+
formatted = render_row(row, resolved, tick_col: tc, highlight: highlight_rows.include?(i),
|
|
77
|
+
ghost: ghost_rows.include?(i), dim: dim_rows.include?(i))
|
|
73
78
|
lines << formatted
|
|
74
79
|
end
|
|
75
80
|
lines.join("\n")
|
|
@@ -77,6 +82,25 @@ module Railbow
|
|
|
77
82
|
|
|
78
83
|
private
|
|
79
84
|
|
|
85
|
+
# Everything that changes a cell's content, and therefore its width,
|
|
86
|
+
# before widths are resolved: hidden columns, value aliases, and the
|
|
87
|
+
# pre-truncation of non-last columns that cap themselves.
|
|
88
|
+
def prepare_rows(rows)
|
|
89
|
+
rows = remap_rows(rows) if @hidden_indices&.any?
|
|
90
|
+
rows = apply_value_aliases(rows) if @aliases[:values]&.any?
|
|
91
|
+
|
|
92
|
+
rows.map { |row|
|
|
93
|
+
row.each_with_index.map { |cell, i|
|
|
94
|
+
col = columns[i]
|
|
95
|
+
if col&.truncate && col.max_width && i < columns.size - 1
|
|
96
|
+
truncate_str(cell.to_s, col.max_width)
|
|
97
|
+
else
|
|
98
|
+
cell
|
|
99
|
+
end
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
80
104
|
def resolve_widths(rows)
|
|
81
105
|
all_rows = rows
|
|
82
106
|
last = columns.size - 1
|
|
@@ -95,6 +119,9 @@ module Railbow
|
|
|
95
119
|
w = [w, col.min_width].max if col.min_width
|
|
96
120
|
w = [w, col.max_width].min if col.max_width
|
|
97
121
|
end
|
|
122
|
+
# Alignment across tables raises the width; an explicit maxw caps it
|
|
123
|
+
# afterwards, so the user's cap stays authoritative.
|
|
124
|
+
w = [w, @min_widths[i].to_i].max if @min_widths && i != last && w > 0
|
|
98
125
|
w = [w, global_maxw].min if global_maxw && i != last && w > 0
|
|
99
126
|
w
|
|
100
127
|
end
|
|
@@ -112,44 +139,65 @@ module Railbow
|
|
|
112
139
|
}.join(theme.header_col_separator)
|
|
113
140
|
end
|
|
114
141
|
|
|
115
|
-
|
|
142
|
+
# A separator row is furniture, not data: the walls are drawn empty and the
|
|
143
|
+
# label is laid over them starting at the second column. Writing over the
|
|
144
|
+
# blanks rather than filling a cell means a label wider than its column
|
|
145
|
+
# spills into the empty space to its right instead of shifting the row.
|
|
146
|
+
def render_separator_row(label, widths, tick_col: nil)
|
|
147
|
+
pad = effective_padding
|
|
148
|
+
cells = widths.map { |w| "#{pad}#{" " * w}#{pad}" }
|
|
149
|
+
|
|
150
|
+
skeleton = cells.first.to_s.dup
|
|
151
|
+
(1...cells.size).each do |i|
|
|
152
|
+
skeleton << "#{separator_at(i - 1, tick_col, cross: true)}#{cells[i]}"
|
|
153
|
+
end
|
|
154
|
+
return skeleton if cells.size < 2 || label.nil? || label.empty?
|
|
155
|
+
|
|
156
|
+
offset = display_width(cells[0]) + display_width(separator_at(0, tick_col, cross: true)) + display_width(pad)
|
|
157
|
+
tail = skeleton[(offset + display_width(label))..] || ""
|
|
158
|
+
"#{skeleton[0, offset]}#{theme.format_separator.call(label)}#{tail}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# The separator character between column index and index + 1. Columns
|
|
162
|
+
# flanking the tick column get the tick variant so the week line reads
|
|
163
|
+
# as one continuous rule.
|
|
164
|
+
def separator_at(index, tick_col, cross: false)
|
|
165
|
+
flanks_tick = tick_col && (index == tick_col - 1 || index == tick_col)
|
|
166
|
+
return theme.col_separator unless flanks_tick
|
|
167
|
+
|
|
168
|
+
cross ? theme.tick_cross_separator : theme.tick_separator
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def render_row(row, widths, tick_col: nil, highlight: false, ghost: false, dim: false)
|
|
116
172
|
last = columns.size - 1
|
|
117
173
|
pad = effective_padding
|
|
118
|
-
default_sep = theme.col_separator
|
|
119
|
-
tick_sep = tick_cross ? theme.tick_cross_separator : theme.tick_separator
|
|
120
174
|
|
|
121
175
|
prefix_parts = row[0...last].each_with_index.map { |cell, i|
|
|
122
176
|
s = cell.to_s
|
|
123
177
|
cell_w = display_width(strip_ansi(s))
|
|
124
178
|
padding = " " * [widths[i] - cell_w, 0].max
|
|
125
179
|
content = (columns[i].align == :right) ? "#{padding}#{s}" : "#{s}#{padding}"
|
|
126
|
-
|
|
127
|
-
content = "#{GHOST_BG}#{GHOST_FG}#{content}#{RESET}"
|
|
128
|
-
elsif highlight
|
|
129
|
-
content = "#{WHITE}#{content}#{RESET}"
|
|
130
|
-
end
|
|
180
|
+
content = style_cell(content, highlight: highlight, ghost: ghost, dim: dim, accent: columns[i].accent)
|
|
131
181
|
"#{pad}#{content}#{RESET}#{pad}"
|
|
132
182
|
}
|
|
133
183
|
|
|
134
|
-
# Join prefix parts with per-position separators
|
|
135
|
-
# Loop index i joins column i-1 and column i (
|
|
136
|
-
#
|
|
137
|
-
|
|
184
|
+
# Join prefix parts with per-position separators.
|
|
185
|
+
# Loop index i joins column i-1 and column i (separator index = i-1).
|
|
186
|
+
# dup: appending to prefix_parts.first itself would corrupt the parts
|
|
187
|
+
# that render_last_cell measures for the wrapped-line indent.
|
|
188
|
+
prefix = prefix_parts.first.to_s.dup
|
|
138
189
|
(1...prefix_parts.size).each do |i|
|
|
139
|
-
|
|
140
|
-
sep = (tick_col && (sep_idx == tick_col - 1 || sep_idx == tick_col)) ? tick_sep : default_sep
|
|
141
|
-
prefix << "#{sep}#{prefix_parts[i]}"
|
|
190
|
+
prefix << "#{separator_at(i - 1, tick_col)}#{prefix_parts[i]}"
|
|
142
191
|
end
|
|
143
192
|
|
|
144
193
|
last_cell_raw = row[last].to_s
|
|
145
194
|
|
|
146
195
|
# Separator before the last column has index (last - 1)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
render_last_cell(prefix, prefix_parts, last_cell_raw, widths, last, col_sep: last_sep, highlight: highlight, ghost: ghost)
|
|
196
|
+
last_sep = separator_at(last - 1, tick_col)
|
|
197
|
+
render_last_cell(prefix, prefix_parts, last_cell_raw, widths, last, col_sep: last_sep, highlight: highlight, ghost: ghost, dim: dim)
|
|
150
198
|
end
|
|
151
199
|
|
|
152
|
-
def render_last_cell(prefix, prefix_parts, last_cell_raw, widths, last, col_sep: nil, highlight: false, ghost: false)
|
|
200
|
+
def render_last_cell(prefix, prefix_parts, last_cell_raw, widths, last, col_sep: nil, highlight: false, ghost: false, dim: false)
|
|
153
201
|
pad = effective_padding
|
|
154
202
|
sep = col_sep || theme.col_separator
|
|
155
203
|
|
|
@@ -167,7 +215,7 @@ module Railbow
|
|
|
167
215
|
if columns[last].truncate_fn && last_col_max &&
|
|
168
216
|
display_width(last_cell_plain) > last_col_max
|
|
169
217
|
last_cell_raw = columns[last].truncate_fn.call(last_cell_raw, last_col_max)
|
|
170
|
-
last_cell_raw = style_cell(last_cell_raw, highlight: highlight, ghost: ghost)
|
|
218
|
+
last_cell_raw = style_cell(last_cell_raw, highlight: highlight, ghost: ghost, dim: dim, accent: columns[last].accent)
|
|
171
219
|
return "#{prefix}#{sep}#{pad}#{last_cell_raw}#{RESET}#{pad}"
|
|
172
220
|
end
|
|
173
221
|
|
|
@@ -175,18 +223,18 @@ module Railbow
|
|
|
175
223
|
if columns[last].truncate && !columns[last].max_width && last_col_max &&
|
|
176
224
|
display_width(last_cell_plain) > last_col_max
|
|
177
225
|
last_cell_raw = truncate_by_words(last_cell_raw, last_col_max)
|
|
178
|
-
last_cell_raw = style_cell(last_cell_raw, highlight: highlight, ghost: ghost)
|
|
226
|
+
last_cell_raw = style_cell(last_cell_raw, highlight: highlight, ghost: ghost, dim: dim, accent: columns[last].accent)
|
|
179
227
|
return "#{prefix}#{sep}#{pad}#{last_cell_raw}#{RESET}#{pad}"
|
|
180
228
|
end
|
|
181
229
|
|
|
182
230
|
# In oneline mode, truncate instead of wrapping
|
|
183
231
|
if @compact[:oneline] && last_col_max && display_width(last_cell_plain) > last_col_max
|
|
184
232
|
last_cell_raw = truncate_by_words(last_cell_raw, last_col_max)
|
|
185
|
-
last_cell_raw = style_cell(last_cell_raw, highlight: highlight, ghost: ghost)
|
|
233
|
+
last_cell_raw = style_cell(last_cell_raw, highlight: highlight, ghost: ghost, dim: dim, accent: columns[last].accent)
|
|
186
234
|
return "#{prefix}#{sep}#{pad}#{last_cell_raw}#{RESET}#{pad}"
|
|
187
235
|
end
|
|
188
236
|
|
|
189
|
-
last_cell_raw = style_cell(last_cell_raw, highlight: highlight, ghost: ghost)
|
|
237
|
+
last_cell_raw = style_cell(last_cell_raw, highlight: highlight, ghost: ghost, dim: dim, accent: columns[last].accent)
|
|
190
238
|
|
|
191
239
|
if last_col_max && display_width(strip_ansi(last_cell_raw)) > last_col_max
|
|
192
240
|
blank_prefix = prefix_parts.map { |part|
|
|
@@ -201,9 +249,17 @@ module Railbow
|
|
|
201
249
|
end
|
|
202
250
|
end
|
|
203
251
|
|
|
204
|
-
def style_cell(content, highlight: false, ghost: false)
|
|
252
|
+
def style_cell(content, highlight: false, ghost: false, dim: false, accent: false)
|
|
205
253
|
if ghost
|
|
206
254
|
"#{GHOST_BG}#{GHOST_FG}#{content}#{RESET}"
|
|
255
|
+
elsif dim
|
|
256
|
+
# An accent column keeps its color - on a dimmed row it is the one
|
|
257
|
+
# cell still carrying meaning. Everything else drops its own colors:
|
|
258
|
+
# one flat grey is what reads as "not in effect", and the resets that
|
|
259
|
+
# end each inner color would break the grey run anyway.
|
|
260
|
+
return content if accent
|
|
261
|
+
|
|
262
|
+
"#{DIMMED_FG}#{strip_ansi(content)}#{RESET}"
|
|
207
263
|
elsif highlight
|
|
208
264
|
"#{WHITE}#{content}#{RESET}"
|
|
209
265
|
else
|
|
@@ -315,7 +371,7 @@ module Railbow
|
|
|
315
371
|
current << token
|
|
316
372
|
current_width += token_width
|
|
317
373
|
elsif current_width.zero?
|
|
318
|
-
# Single token wider than max
|
|
374
|
+
# Single token wider than max - try to break on underscores
|
|
319
375
|
broken = break_long_token(token, max_width)
|
|
320
376
|
lines.concat(broken[0...-1])
|
|
321
377
|
current = +broken.last
|
|
@@ -400,6 +456,8 @@ module Railbow
|
|
|
400
456
|
# Build column index → value alias map
|
|
401
457
|
col_map = {}
|
|
402
458
|
columns.each_with_index do |col, i|
|
|
459
|
+
next unless col.aliased
|
|
460
|
+
|
|
403
461
|
label = col.label
|
|
404
462
|
col_map[i] = value_aliases[label] if value_aliases[label]
|
|
405
463
|
# Also look up by original name if column was renamed by alias
|
data/lib/railbow/table/theme.rb
CHANGED
|
@@ -8,7 +8,8 @@ module Railbow
|
|
|
8
8
|
:format_header_cell, :format_separator
|
|
9
9
|
|
|
10
10
|
def initialize(col_separator:, header_col_separator:, cell_padding:,
|
|
11
|
-
format_header_cell:, format_separator: nil,
|
|
11
|
+
format_header_cell:, format_separator: nil,
|
|
12
|
+
tick_separator: nil, tick_cross_separator: nil)
|
|
12
13
|
@col_separator = col_separator
|
|
13
14
|
@tick_separator = tick_separator || col_separator
|
|
14
15
|
@tick_cross_separator = tick_cross_separator || @tick_separator
|
|
@@ -23,7 +24,7 @@ module Railbow
|
|
|
23
24
|
RESET = "\e[0m"
|
|
24
25
|
BOLD = "\e[1m"
|
|
25
26
|
WHITE = "\e[97m"
|
|
26
|
-
PURPLE = "\e[38;5;
|
|
27
|
+
PURPLE = "\e[38;5;97m" # separator rows: present, but never louder than the data
|
|
27
28
|
BG_PURPLE = "\e[48;5;99m"
|
|
28
29
|
DIM = "\e[2m"
|
|
29
30
|
|