railbow 0.1.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 +7 -0
- data/CHANGELOG.md +31 -0
- data/LICENSE.txt +21 -0
- data/README.md +243 -0
- data/exe/railbow +110 -0
- data/lib/railbow/about_formatter.rb +37 -0
- data/lib/railbow/color_assigner.rb +53 -0
- data/lib/railbow/config.rb +113 -0
- data/lib/railbow/demo/fixtures.rb +121 -0
- data/lib/railbow/demo/migrate_demo.rb +35 -0
- data/lib/railbow/demo/routes_demo.rb +94 -0
- data/lib/railbow/demo/runner.rb +51 -0
- data/lib/railbow/demo/status_demo.rb +164 -0
- data/lib/railbow/formatters/base.rb +195 -0
- data/lib/railbow/git_utils.rb +29 -0
- data/lib/railbow/init.rb +112 -0
- data/lib/railbow/logo.rb +27 -0
- data/lib/railbow/migration_formatter.rb +64 -0
- data/lib/railbow/migration_parser.rb +87 -0
- data/lib/railbow/name_collision_resolver.rb +155 -0
- data/lib/railbow/name_formatter.rb +82 -0
- data/lib/railbow/notes_formatter.rb +311 -0
- data/lib/railbow/params.rb +229 -0
- data/lib/railbow/railtie.rb +44 -0
- data/lib/railbow/routes_formatter.rb +163 -0
- data/lib/railbow/stats_formatter.rb +99 -0
- data/lib/railbow/table/column.rb +24 -0
- data/lib/railbow/table/renderer.rb +441 -0
- data/lib/railbow/table/theme.rb +54 -0
- data/lib/railbow/table.rb +5 -0
- data/lib/railbow/tasks/init.rake +10 -0
- data/lib/railbow/tasks/migrate_status.rake +694 -0
- data/lib/railbow/tasks/stats.rake +85 -0
- data/lib/railbow/text_utils.rb +30 -0
- data/lib/railbow/version.rb +5 -0
- data/lib/railbow.rb +22 -0
- data/sig/railbow.rbs +4 -0
- metadata +137 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "io/console"
|
|
4
|
+
require_relative "../text_utils"
|
|
5
|
+
|
|
6
|
+
module Railbow
|
|
7
|
+
module Table
|
|
8
|
+
class Renderer
|
|
9
|
+
include TextUtils
|
|
10
|
+
|
|
11
|
+
RESET = "\e[0m"
|
|
12
|
+
WHITE = "\e[97m"
|
|
13
|
+
GHOST_BG = "\e[48;5;52m" # deep red/maroon background — stands out as abnormal
|
|
14
|
+
GHOST_FG = "\e[38;5;217m" # warm pink foreground for contrast
|
|
15
|
+
|
|
16
|
+
attr_reader :columns, :theme
|
|
17
|
+
|
|
18
|
+
def initialize(columns:, theme:, compact: {}, aliases: {})
|
|
19
|
+
@compact = compact
|
|
20
|
+
@aliases = aliases
|
|
21
|
+
@reverse_col_aliases = aliases[:columns]&.invert || {}
|
|
22
|
+
@columns = apply_hidden_columns(columns)
|
|
23
|
+
@theme = theme
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def render(rows, separators: {}, highlight_rows: Set.new, ghost_rows: Set.new, tick_rows: Set.new, tick_col: nil)
|
|
27
|
+
return "" if columns.empty?
|
|
28
|
+
|
|
29
|
+
# Remap rows if columns were hidden
|
|
30
|
+
rows = remap_rows(rows) if @hidden_indices&.any?
|
|
31
|
+
|
|
32
|
+
# Apply value aliases
|
|
33
|
+
rows = apply_value_aliases(rows) if @aliases[:values]&.any?
|
|
34
|
+
|
|
35
|
+
# Pre-truncate non-last columns that have truncate + max_width
|
|
36
|
+
rows = rows.map { |row|
|
|
37
|
+
row.each_with_index.map { |cell, i|
|
|
38
|
+
col = columns[i]
|
|
39
|
+
if col&.truncate && col.max_width && i < columns.size - 1
|
|
40
|
+
truncate_str(cell.to_s, col.max_width)
|
|
41
|
+
else
|
|
42
|
+
cell
|
|
43
|
+
end
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
resolved = resolve_widths(rows)
|
|
48
|
+
|
|
49
|
+
# In oneline mode, truncate non-sticky non-last columns at resolved width
|
|
50
|
+
if @compact[:oneline]
|
|
51
|
+
rows = rows.map { |row|
|
|
52
|
+
row.each_with_index.map { |cell, i|
|
|
53
|
+
col = columns[i]
|
|
54
|
+
if col && i < columns.size - 1 && !col.sticky && !col.truncate
|
|
55
|
+
truncate_str(cell.to_s, resolved[i])
|
|
56
|
+
else
|
|
57
|
+
cell
|
|
58
|
+
end
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
lines = []
|
|
63
|
+
lines << render_header(resolved) unless @compact[:noheader]
|
|
64
|
+
rows.each_with_index do |row, i|
|
|
65
|
+
tc = (tick_rows.include?(i) && tick_col) ? tick_col : nil
|
|
66
|
+
if separators.key?(i) && theme.format_separator
|
|
67
|
+
sep_row = Array.new(columns.size, "")
|
|
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)
|
|
70
|
+
tc = nil # tick already shown on separator row
|
|
71
|
+
end
|
|
72
|
+
formatted = render_row(row, resolved, tick_col: tc, highlight: highlight_rows.include?(i), ghost: ghost_rows.include?(i))
|
|
73
|
+
lines << formatted
|
|
74
|
+
end
|
|
75
|
+
lines.join("\n")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def resolve_widths(rows)
|
|
81
|
+
all_rows = rows
|
|
82
|
+
last = columns.size - 1
|
|
83
|
+
global_maxw = @compact[:maxw]
|
|
84
|
+
|
|
85
|
+
columns.each_with_index.map do |col, i|
|
|
86
|
+
if col.fixed?
|
|
87
|
+
w = col.width
|
|
88
|
+
elsif i == last
|
|
89
|
+
# Last column: don't pad, will be wrapped if it overflows
|
|
90
|
+
w = 0
|
|
91
|
+
else
|
|
92
|
+
header_w = display_width(effective_label(col))
|
|
93
|
+
content_w = all_rows.map { |row| display_width(strip_ansi(row[i].to_s)) }.max || 0
|
|
94
|
+
w = [header_w, content_w].max
|
|
95
|
+
w = [w, col.min_width].max if col.min_width
|
|
96
|
+
w = [w, col.max_width].min if col.max_width
|
|
97
|
+
end
|
|
98
|
+
w = [w, global_maxw].min if global_maxw && i != last && w > 0
|
|
99
|
+
w
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def render_header(widths)
|
|
104
|
+
last = columns.size - 1
|
|
105
|
+
pad = effective_padding
|
|
106
|
+
|
|
107
|
+
columns.each_with_index.map { |col, i|
|
|
108
|
+
text = effective_label(col)
|
|
109
|
+
padding = (i == last) ? "" : " " * [widths[i] - display_width(text), 0].max
|
|
110
|
+
cell = theme.format_header_cell.call("#{text}#{padding}", padding)
|
|
111
|
+
"#{pad}#{cell}#{pad}"
|
|
112
|
+
}.join(theme.header_col_separator)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def render_row(row, widths, tick_col: nil, tick_cross: false, highlight: false, ghost: false)
|
|
116
|
+
last = columns.size - 1
|
|
117
|
+
pad = effective_padding
|
|
118
|
+
default_sep = theme.col_separator
|
|
119
|
+
tick_sep = tick_cross ? theme.tick_cross_separator : theme.tick_separator
|
|
120
|
+
|
|
121
|
+
prefix_parts = row[0...last].each_with_index.map { |cell, i|
|
|
122
|
+
s = cell.to_s
|
|
123
|
+
cell_w = display_width(strip_ansi(s))
|
|
124
|
+
padding = " " * [widths[i] - cell_w, 0].max
|
|
125
|
+
content = (columns[i].align == :right) ? "#{padding}#{s}" : "#{s}#{padding}"
|
|
126
|
+
if ghost
|
|
127
|
+
content = "#{GHOST_BG}#{GHOST_FG}#{content}#{RESET}"
|
|
128
|
+
elsif highlight
|
|
129
|
+
content = "#{WHITE}#{content}#{RESET}"
|
|
130
|
+
end
|
|
131
|
+
"#{pad}#{content}#{RESET}#{pad}"
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# Join prefix parts with per-position separators
|
|
135
|
+
# Loop index i joins column i-1 and column i (separator_index = i-1)
|
|
136
|
+
# For tick_col, flanking separator indices are tick_col-1 and tick_col
|
|
137
|
+
prefix = prefix_parts.first.to_s
|
|
138
|
+
(1...prefix_parts.size).each do |i|
|
|
139
|
+
sep_idx = i - 1
|
|
140
|
+
sep = (tick_col && (sep_idx == tick_col - 1 || sep_idx == tick_col)) ? tick_sep : default_sep
|
|
141
|
+
prefix << "#{sep}#{prefix_parts[i]}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
last_cell_raw = row[last].to_s
|
|
145
|
+
|
|
146
|
+
# Separator before the last column has index (last - 1)
|
|
147
|
+
last_sep_idx = last - 1
|
|
148
|
+
last_sep = (tick_col && (last_sep_idx == tick_col - 1 || last_sep_idx == tick_col)) ? tick_sep : default_sep
|
|
149
|
+
render_last_cell(prefix, prefix_parts, last_cell_raw, widths, last, col_sep: last_sep, highlight: highlight, ghost: ghost)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def render_last_cell(prefix, prefix_parts, last_cell_raw, widths, last, col_sep: nil, highlight: false, ghost: false)
|
|
153
|
+
pad = effective_padding
|
|
154
|
+
sep = col_sep || theme.col_separator
|
|
155
|
+
|
|
156
|
+
# Truncate if configured via column settings
|
|
157
|
+
if columns[last].truncate && columns[last].max_width
|
|
158
|
+
last_cell_raw = truncate_str(last_cell_raw, columns[last].max_width)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
last_cell_plain = strip_ansi(last_cell_raw)
|
|
162
|
+
term_w = terminal_width
|
|
163
|
+
prefix_width = compute_prefix_width(widths, last)
|
|
164
|
+
last_col_max = term_w ? [term_w - prefix_width - display_width(pad), 10].max : nil
|
|
165
|
+
|
|
166
|
+
# Use custom truncate_fn if available (e.g. table tags with +N)
|
|
167
|
+
if columns[last].truncate_fn && last_col_max &&
|
|
168
|
+
display_width(last_cell_plain) > last_col_max
|
|
169
|
+
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)
|
|
171
|
+
return "#{prefix}#{sep}#{pad}#{last_cell_raw}#{RESET}#{pad}"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Truncate to terminal width (by whole words) instead of wrapping
|
|
175
|
+
if columns[last].truncate && !columns[last].max_width && last_col_max &&
|
|
176
|
+
display_width(last_cell_plain) > last_col_max
|
|
177
|
+
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)
|
|
179
|
+
return "#{prefix}#{sep}#{pad}#{last_cell_raw}#{RESET}#{pad}"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# In oneline mode, truncate instead of wrapping
|
|
183
|
+
if @compact[:oneline] && last_col_max && display_width(last_cell_plain) > last_col_max
|
|
184
|
+
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)
|
|
186
|
+
return "#{prefix}#{sep}#{pad}#{last_cell_raw}#{RESET}#{pad}"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
last_cell_raw = style_cell(last_cell_raw, highlight: highlight, ghost: ghost)
|
|
190
|
+
|
|
191
|
+
if last_col_max && display_width(strip_ansi(last_cell_raw)) > last_col_max
|
|
192
|
+
blank_prefix = prefix_parts.map { |part|
|
|
193
|
+
" " * display_width(strip_ansi(part))
|
|
194
|
+
}.join(sep)
|
|
195
|
+
|
|
196
|
+
wrapped = ansi_word_wrap(last_cell_raw, last_col_max)
|
|
197
|
+
"#{prefix}#{sep}#{pad}#{wrapped.first}#{RESET}#{pad}\n" +
|
|
198
|
+
wrapped[1..].map { |line| "#{blank_prefix}#{sep}#{pad}#{line}#{RESET}#{pad}" }.join("\n")
|
|
199
|
+
else
|
|
200
|
+
"#{prefix}#{sep}#{pad}#{last_cell_raw}#{RESET}#{pad}"
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def style_cell(content, highlight: false, ghost: false)
|
|
205
|
+
if ghost
|
|
206
|
+
"#{GHOST_BG}#{GHOST_FG}#{content}#{RESET}"
|
|
207
|
+
elsif highlight
|
|
208
|
+
"#{WHITE}#{content}#{RESET}"
|
|
209
|
+
else
|
|
210
|
+
content
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def compute_prefix_width(widths, last)
|
|
215
|
+
pad_w = display_width(effective_padding)
|
|
216
|
+
sep_w = display_width(theme.col_separator)
|
|
217
|
+
# Each non-last column: pad + content + pad, joined by separator
|
|
218
|
+
total = 0
|
|
219
|
+
(0...last).each do |i|
|
|
220
|
+
total += pad_w + widths[i] + pad_w
|
|
221
|
+
end
|
|
222
|
+
# Separators between columns + trailing separator before last column
|
|
223
|
+
total += sep_w * last
|
|
224
|
+
# Plus pad on last column
|
|
225
|
+
total += pad_w
|
|
226
|
+
total
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def truncate_by_words(str, max_width)
|
|
230
|
+
return str if display_width(strip_ansi(str)) <= max_width
|
|
231
|
+
|
|
232
|
+
segments = str.scan(/\S+\s*/)
|
|
233
|
+
result = +""
|
|
234
|
+
width = 0
|
|
235
|
+
|
|
236
|
+
segments.each do |seg|
|
|
237
|
+
seg_plain = strip_ansi(seg)
|
|
238
|
+
seg_width = display_width(seg_plain)
|
|
239
|
+
if width + seg_width + 3 > max_width && width > 0
|
|
240
|
+
result.rstrip!
|
|
241
|
+
result << "..."
|
|
242
|
+
return result
|
|
243
|
+
end
|
|
244
|
+
result << seg
|
|
245
|
+
width += seg_width
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
result
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def terminal_width
|
|
252
|
+
return $stdout.winsize[1] if $stdout.respond_to?(:winsize) && $stdout.tty?
|
|
253
|
+
nil
|
|
254
|
+
rescue
|
|
255
|
+
nil
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def ansi_word_wrap(str, max_width)
|
|
259
|
+
plain = strip_ansi(str)
|
|
260
|
+
plain_lines = word_wrap(plain, max_width)
|
|
261
|
+
|
|
262
|
+
result = []
|
|
263
|
+
pos = 0
|
|
264
|
+
last_color = nil
|
|
265
|
+
plain_lines.each do |plain_line|
|
|
266
|
+
target = plain_line.lstrip
|
|
267
|
+
line = +""
|
|
268
|
+
line << last_color if last_color
|
|
269
|
+
visible_consumed = 0
|
|
270
|
+
skipping_leading = true
|
|
271
|
+
|
|
272
|
+
while pos < str.length && visible_consumed < display_width(target)
|
|
273
|
+
if str[pos] == "\e"
|
|
274
|
+
esc_end = str.index("m", pos) || pos
|
|
275
|
+
code = str[pos..esc_end]
|
|
276
|
+
line << code
|
|
277
|
+
last_color = (code == RESET) ? nil : code
|
|
278
|
+
pos = esc_end + 1
|
|
279
|
+
else
|
|
280
|
+
ch = str[pos]
|
|
281
|
+
unless skipping_leading && ch.match?(/\s/) && visible_consumed == 0
|
|
282
|
+
skipping_leading = false
|
|
283
|
+
line << ch
|
|
284
|
+
visible_consumed += display_width(ch)
|
|
285
|
+
end
|
|
286
|
+
pos += 1
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
while pos < str.length && str[pos] == "\e"
|
|
291
|
+
esc_end = str.index("m", pos) || pos
|
|
292
|
+
code = str[pos..esc_end]
|
|
293
|
+
line << code
|
|
294
|
+
last_color = (code == RESET) ? nil : code
|
|
295
|
+
pos = esc_end + 1
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
result << line
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
result
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def word_wrap(str, max_width)
|
|
305
|
+
return [str] if display_width(str) <= max_width
|
|
306
|
+
|
|
307
|
+
lines = []
|
|
308
|
+
current = +""
|
|
309
|
+
current_width = 0
|
|
310
|
+
|
|
311
|
+
str.split(/(\s+)/).each do |token|
|
|
312
|
+
token_width = display_width(token)
|
|
313
|
+
|
|
314
|
+
if current_width + token_width <= max_width
|
|
315
|
+
current << token
|
|
316
|
+
current_width += token_width
|
|
317
|
+
elsif current_width.zero?
|
|
318
|
+
# Single token wider than max — try to break on underscores
|
|
319
|
+
broken = break_long_token(token, max_width)
|
|
320
|
+
lines.concat(broken[0...-1])
|
|
321
|
+
current = +broken.last
|
|
322
|
+
current_width = display_width(current)
|
|
323
|
+
else
|
|
324
|
+
lines << current.rstrip
|
|
325
|
+
token = token.lstrip
|
|
326
|
+
current = +token
|
|
327
|
+
current_width = display_width(token)
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
lines << current.rstrip unless current.strip.empty?
|
|
332
|
+
lines
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def break_long_token(token, max_width)
|
|
336
|
+
return [token] unless token.include?("_")
|
|
337
|
+
|
|
338
|
+
parts = token.split(/(?<=_)/) # split keeping _ at end of each part
|
|
339
|
+
lines = []
|
|
340
|
+
current = +""
|
|
341
|
+
current_width = 0
|
|
342
|
+
|
|
343
|
+
parts.each do |part|
|
|
344
|
+
part_width = display_width(part)
|
|
345
|
+
if current_width + part_width <= max_width
|
|
346
|
+
current << part
|
|
347
|
+
current_width += part_width
|
|
348
|
+
elsif current_width.zero?
|
|
349
|
+
lines << part
|
|
350
|
+
else
|
|
351
|
+
lines << current
|
|
352
|
+
current = +part
|
|
353
|
+
current_width = part_width
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
lines << current unless current.empty?
|
|
358
|
+
lines.empty? ? [token] : lines
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# --- Compact support ---
|
|
362
|
+
|
|
363
|
+
def effective_padding
|
|
364
|
+
@compact[:dense] ? "" : theme.cell_padding
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def effective_label(col)
|
|
368
|
+
col_aliases = @aliases[:columns]
|
|
369
|
+
return col.label unless col_aliases
|
|
370
|
+
|
|
371
|
+
col_aliases[col.label] || col.label
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def apply_hidden_columns(columns)
|
|
375
|
+
hidden = @compact[:hidden_columns]
|
|
376
|
+
return columns unless hidden&.any?
|
|
377
|
+
|
|
378
|
+
@hidden_indices = []
|
|
379
|
+
filtered = []
|
|
380
|
+
columns.each_with_index do |col, i|
|
|
381
|
+
if hidden.any? { |h| h.downcase == col.label.downcase }
|
|
382
|
+
@hidden_indices << i
|
|
383
|
+
else
|
|
384
|
+
filtered << col
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
filtered
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def remap_rows(rows)
|
|
391
|
+
rows.map do |row|
|
|
392
|
+
row.each_with_index.reject { |_, i| @hidden_indices.include?(i) }.map(&:first)
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def apply_value_aliases(rows)
|
|
397
|
+
value_aliases = @aliases[:values]
|
|
398
|
+
return rows unless value_aliases&.any?
|
|
399
|
+
|
|
400
|
+
# Build column index → value alias map
|
|
401
|
+
col_map = {}
|
|
402
|
+
columns.each_with_index do |col, i|
|
|
403
|
+
label = col.label
|
|
404
|
+
col_map[i] = value_aliases[label] if value_aliases[label]
|
|
405
|
+
# Also look up by original name if column was renamed by alias
|
|
406
|
+
original = @reverse_col_aliases[label]
|
|
407
|
+
col_map[i] = value_aliases[original] if original && value_aliases[original]
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
return rows if col_map.empty?
|
|
411
|
+
|
|
412
|
+
rows.map do |row|
|
|
413
|
+
row.each_with_index.map do |cell, i|
|
|
414
|
+
aliases_for_col = col_map[i]
|
|
415
|
+
if aliases_for_col
|
|
416
|
+
apply_cell_alias(cell.to_s, aliases_for_col)
|
|
417
|
+
else
|
|
418
|
+
cell
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def apply_cell_alias(cell, aliases_for_col)
|
|
425
|
+
plain = strip_ansi(cell)
|
|
426
|
+
# Try exact match first, then prefix match for cells with appended indicators
|
|
427
|
+
replacement = aliases_for_col[plain]
|
|
428
|
+
if replacement
|
|
429
|
+
cell.sub(plain) { replacement }
|
|
430
|
+
else
|
|
431
|
+
key = aliases_for_col.keys.find { |k| plain.start_with?(k) && plain[k.length..] =~ /\A\s/ }
|
|
432
|
+
if key
|
|
433
|
+
cell.sub(key) { aliases_for_col[key] }
|
|
434
|
+
else
|
|
435
|
+
cell
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railbow
|
|
4
|
+
module Table
|
|
5
|
+
class Theme
|
|
6
|
+
attr_reader :col_separator, :tick_separator, :tick_cross_separator,
|
|
7
|
+
:header_col_separator, :cell_padding,
|
|
8
|
+
:format_header_cell, :format_separator
|
|
9
|
+
|
|
10
|
+
def initialize(col_separator:, header_col_separator:, cell_padding:,
|
|
11
|
+
format_header_cell:, format_separator: nil, tick_separator: nil, tick_cross_separator: nil)
|
|
12
|
+
@col_separator = col_separator
|
|
13
|
+
@tick_separator = tick_separator || col_separator
|
|
14
|
+
@tick_cross_separator = tick_cross_separator || @tick_separator
|
|
15
|
+
@header_col_separator = header_col_separator
|
|
16
|
+
@cell_padding = cell_padding
|
|
17
|
+
@format_header_cell = format_header_cell
|
|
18
|
+
@format_separator = format_separator
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
module Themes
|
|
23
|
+
RESET = "\e[0m"
|
|
24
|
+
BOLD = "\e[1m"
|
|
25
|
+
WHITE = "\e[97m"
|
|
26
|
+
PURPLE = "\e[38;5;141m"
|
|
27
|
+
BG_PURPLE = "\e[48;5;99m"
|
|
28
|
+
DIM = "\e[2m"
|
|
29
|
+
|
|
30
|
+
WALLS = Theme.new(
|
|
31
|
+
col_separator: "\u2502",
|
|
32
|
+
tick_separator: "\u252c",
|
|
33
|
+
tick_cross_separator: "\u253c",
|
|
34
|
+
header_col_separator: " ",
|
|
35
|
+
cell_padding: " ",
|
|
36
|
+
format_header_cell: ->(text, _padding) {
|
|
37
|
+
"#{BG_PURPLE}#{BOLD}#{WHITE}#{text}#{RESET}"
|
|
38
|
+
},
|
|
39
|
+
format_separator: ->(label) {
|
|
40
|
+
"#{PURPLE}#{label}#{RESET}"
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
PLAIN = Theme.new(
|
|
45
|
+
col_separator: " ",
|
|
46
|
+
header_col_separator: " ",
|
|
47
|
+
cell_padding: "",
|
|
48
|
+
format_header_cell: ->(text, _padding) {
|
|
49
|
+
"#{BOLD}#{text}#{RESET}"
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|