asciinema_win 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/README.md +575 -0
- data/exe/asciinema_win +17 -0
- data/lib/asciinema_win/ansi_parser.rb +437 -0
- data/lib/asciinema_win/asciicast.rb +537 -0
- data/lib/asciinema_win/cli.rb +591 -0
- data/lib/asciinema_win/export.rb +780 -0
- data/lib/asciinema_win/output_organizer.rb +276 -0
- data/lib/asciinema_win/player.rb +348 -0
- data/lib/asciinema_win/recorder.rb +480 -0
- data/lib/asciinema_win/screen_buffer.rb +375 -0
- data/lib/asciinema_win/themes.rb +334 -0
- data/lib/asciinema_win/version.rb +6 -0
- data/lib/asciinema_win.rb +153 -0
- data/lib/rich/_palettes.rb +148 -0
- data/lib/rich/box.rb +342 -0
- data/lib/rich/cells.rb +512 -0
- data/lib/rich/color.rb +628 -0
- data/lib/rich/color_triplet.rb +220 -0
- data/lib/rich/console.rb +549 -0
- data/lib/rich/control.rb +332 -0
- data/lib/rich/json.rb +254 -0
- data/lib/rich/layout.rb +314 -0
- data/lib/rich/markdown.rb +509 -0
- data/lib/rich/markup.rb +175 -0
- data/lib/rich/panel.rb +311 -0
- data/lib/rich/progress.rb +430 -0
- data/lib/rich/segment.rb +387 -0
- data/lib/rich/style.rb +433 -0
- data/lib/rich/syntax.rb +1145 -0
- data/lib/rich/table.rb +525 -0
- data/lib/rich/terminal_theme.rb +126 -0
- data/lib/rich/text.rb +433 -0
- data/lib/rich/tree.rb +220 -0
- data/lib/rich/version.rb +5 -0
- data/lib/rich/win32_console.rb +859 -0
- data/lib/rich.rb +108 -0
- metadata +123 -0
data/lib/rich/table.rb
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "box"
|
|
4
|
+
require_relative "style"
|
|
5
|
+
require_relative "segment"
|
|
6
|
+
require_relative "cells"
|
|
7
|
+
require_relative "text"
|
|
8
|
+
|
|
9
|
+
module Rich
|
|
10
|
+
# Column definition for a Table
|
|
11
|
+
class Column
|
|
12
|
+
# @return [String] Column header
|
|
13
|
+
attr_reader :header
|
|
14
|
+
|
|
15
|
+
# @return [String, nil] Column footer
|
|
16
|
+
attr_reader :footer
|
|
17
|
+
|
|
18
|
+
# @return [Style, nil] Header style
|
|
19
|
+
attr_reader :header_style
|
|
20
|
+
|
|
21
|
+
# @return [Style, nil] Cell style
|
|
22
|
+
attr_reader :style
|
|
23
|
+
|
|
24
|
+
# @return [Style, nil] Footer style
|
|
25
|
+
attr_reader :footer_style
|
|
26
|
+
|
|
27
|
+
# @return [Symbol] Justification (:left, :center, :right)
|
|
28
|
+
attr_reader :justify
|
|
29
|
+
|
|
30
|
+
# @return [Integer, nil] Minimum width
|
|
31
|
+
attr_reader :min_width
|
|
32
|
+
|
|
33
|
+
# @return [Integer, nil] Maximum width
|
|
34
|
+
attr_reader :max_width
|
|
35
|
+
|
|
36
|
+
# @return [Boolean] No wrap
|
|
37
|
+
attr_reader :no_wrap
|
|
38
|
+
|
|
39
|
+
# @return [Symbol] Overflow handling (:fold, :crop, :ellipsis)
|
|
40
|
+
attr_reader :overflow
|
|
41
|
+
|
|
42
|
+
# @return [Integer] Ratio for flexible sizing
|
|
43
|
+
attr_reader :ratio
|
|
44
|
+
|
|
45
|
+
def initialize(
|
|
46
|
+
header = "",
|
|
47
|
+
footer: nil,
|
|
48
|
+
header_style: nil,
|
|
49
|
+
style: nil,
|
|
50
|
+
footer_style: nil,
|
|
51
|
+
justify: :left,
|
|
52
|
+
min_width: nil,
|
|
53
|
+
max_width: nil,
|
|
54
|
+
no_wrap: false,
|
|
55
|
+
overflow: :ellipsis,
|
|
56
|
+
ratio: 1
|
|
57
|
+
)
|
|
58
|
+
@header = header.to_s
|
|
59
|
+
@footer = footer
|
|
60
|
+
@header_style = header_style.is_a?(String) ? Style.parse(header_style) : header_style
|
|
61
|
+
@style = style.is_a?(String) ? Style.parse(style) : style
|
|
62
|
+
@footer_style = footer_style.is_a?(String) ? Style.parse(footer_style) : footer_style
|
|
63
|
+
@justify = justify
|
|
64
|
+
@min_width = min_width
|
|
65
|
+
@max_width = max_width
|
|
66
|
+
@no_wrap = no_wrap
|
|
67
|
+
@overflow = overflow
|
|
68
|
+
@ratio = ratio
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# A table for displaying tabular data
|
|
73
|
+
class Table
|
|
74
|
+
# @return [String, nil] Table title
|
|
75
|
+
attr_reader :title
|
|
76
|
+
|
|
77
|
+
# @return [String, nil] Table caption
|
|
78
|
+
attr_reader :caption
|
|
79
|
+
|
|
80
|
+
# @return [Box] Box style
|
|
81
|
+
attr_reader :box
|
|
82
|
+
|
|
83
|
+
# @return [Style, nil] Border style
|
|
84
|
+
attr_reader :border_style
|
|
85
|
+
|
|
86
|
+
# @return [Style, nil] Header style
|
|
87
|
+
attr_reader :header_style
|
|
88
|
+
|
|
89
|
+
# @return [Style, nil] Title style
|
|
90
|
+
attr_reader :title_style
|
|
91
|
+
|
|
92
|
+
# @return [Style, nil] Caption style
|
|
93
|
+
attr_reader :caption_style
|
|
94
|
+
|
|
95
|
+
# @return [Style, nil] Row styles (alternating)
|
|
96
|
+
attr_reader :row_styles
|
|
97
|
+
|
|
98
|
+
# @return [Boolean] Show header
|
|
99
|
+
attr_reader :show_header
|
|
100
|
+
|
|
101
|
+
# @return [Boolean] Show footer
|
|
102
|
+
attr_reader :show_footer
|
|
103
|
+
|
|
104
|
+
# @return [Boolean] Show edge (outer border)
|
|
105
|
+
attr_reader :show_edge
|
|
106
|
+
|
|
107
|
+
# @return [Boolean] Show lines between rows
|
|
108
|
+
attr_reader :show_lines
|
|
109
|
+
|
|
110
|
+
# @return [Integer] Padding
|
|
111
|
+
attr_reader :padding
|
|
112
|
+
|
|
113
|
+
# @return [Boolean] Expand to full width
|
|
114
|
+
attr_reader :expand
|
|
115
|
+
|
|
116
|
+
# @return [Integer, nil] Fixed width
|
|
117
|
+
attr_reader :width
|
|
118
|
+
|
|
119
|
+
# @return [Array<Column>] Columns
|
|
120
|
+
attr_reader :columns
|
|
121
|
+
|
|
122
|
+
# @return [Array<Array<String>>] Rows
|
|
123
|
+
attr_reader :rows
|
|
124
|
+
|
|
125
|
+
def initialize(
|
|
126
|
+
title: nil,
|
|
127
|
+
caption: nil,
|
|
128
|
+
box: Box::ROUNDED,
|
|
129
|
+
border_style: nil,
|
|
130
|
+
header_style: nil,
|
|
131
|
+
title_style: nil,
|
|
132
|
+
caption_style: nil,
|
|
133
|
+
row_styles: nil,
|
|
134
|
+
show_header: true,
|
|
135
|
+
show_footer: false,
|
|
136
|
+
show_edge: true,
|
|
137
|
+
show_lines: false,
|
|
138
|
+
padding: 1,
|
|
139
|
+
expand: false,
|
|
140
|
+
width: nil
|
|
141
|
+
)
|
|
142
|
+
@title = title
|
|
143
|
+
@caption = caption
|
|
144
|
+
@box = box
|
|
145
|
+
@border_style = border_style.is_a?(String) ? Style.parse(border_style) : border_style
|
|
146
|
+
@header_style = header_style.is_a?(String) ? Style.parse(header_style) : header_style
|
|
147
|
+
@title_style = title_style.is_a?(String) ? Style.parse(title_style) : title_style
|
|
148
|
+
@caption_style = caption_style.is_a?(String) ? Style.parse(caption_style) : caption_style
|
|
149
|
+
@row_styles = row_styles
|
|
150
|
+
@show_header = show_header
|
|
151
|
+
@show_footer = show_footer
|
|
152
|
+
@show_edge = show_edge
|
|
153
|
+
@show_lines = show_lines
|
|
154
|
+
@padding = padding
|
|
155
|
+
@expand = expand
|
|
156
|
+
@width = width
|
|
157
|
+
|
|
158
|
+
@columns = []
|
|
159
|
+
@rows = []
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Add a column
|
|
163
|
+
# @param header [String] Column header
|
|
164
|
+
# @param kwargs [Hash] Column options
|
|
165
|
+
# @return [self]
|
|
166
|
+
def add_column(header = "", **kwargs)
|
|
167
|
+
@columns << Column.new(header, **kwargs)
|
|
168
|
+
self
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Add a row
|
|
172
|
+
# @param cells [Array] Cell contents
|
|
173
|
+
# @return [self]
|
|
174
|
+
def add_row(*cells)
|
|
175
|
+
# Ensure we have enough columns
|
|
176
|
+
while @columns.length < cells.length
|
|
177
|
+
@columns << Column.new
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
@rows << cells.map(&:to_s)
|
|
181
|
+
self
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# @return [Integer] Number of columns
|
|
185
|
+
def column_count
|
|
186
|
+
@columns.length
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# @return [Integer] Number of rows
|
|
190
|
+
def row_count
|
|
191
|
+
@rows.length
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Render table to segments
|
|
195
|
+
# @param max_width [Integer] Maximum width
|
|
196
|
+
# @return [Array<Segment>]
|
|
197
|
+
def to_segments(max_width: 80)
|
|
198
|
+
return [Segment.new("")] if @columns.empty?
|
|
199
|
+
|
|
200
|
+
segments = []
|
|
201
|
+
col_widths = calculate_column_widths(max_width)
|
|
202
|
+
table_width = col_widths.sum + (@columns.length + 1) + @columns.length * @padding * 2
|
|
203
|
+
|
|
204
|
+
# Title
|
|
205
|
+
if @title && @show_edge
|
|
206
|
+
segments.concat(render_title(table_width - 2))
|
|
207
|
+
segments << Segment.new("\n")
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Top border
|
|
211
|
+
if @show_edge
|
|
212
|
+
segments.concat(render_top_border(col_widths))
|
|
213
|
+
segments << Segment.new("\n")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Header
|
|
217
|
+
if @show_header
|
|
218
|
+
segments.concat(render_header_row(col_widths))
|
|
219
|
+
segments << Segment.new("\n")
|
|
220
|
+
|
|
221
|
+
# Header separator
|
|
222
|
+
segments.concat(render_header_separator(col_widths))
|
|
223
|
+
segments << Segment.new("\n")
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Data rows
|
|
227
|
+
@rows.each_with_index do |row, index|
|
|
228
|
+
segments.concat(render_data_row(row, col_widths, index))
|
|
229
|
+
segments << Segment.new("\n")
|
|
230
|
+
|
|
231
|
+
# Row separator
|
|
232
|
+
if @show_lines && index < @rows.length - 1
|
|
233
|
+
segments.concat(render_row_separator(col_widths))
|
|
234
|
+
segments << Segment.new("\n")
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Footer
|
|
239
|
+
if @show_footer && @columns.any? { |c| c.footer }
|
|
240
|
+
segments.concat(render_footer_separator(col_widths))
|
|
241
|
+
segments << Segment.new("\n")
|
|
242
|
+
segments.concat(render_footer_row(col_widths))
|
|
243
|
+
segments << Segment.new("\n")
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Bottom border
|
|
247
|
+
if @show_edge
|
|
248
|
+
segments.concat(render_bottom_border(col_widths))
|
|
249
|
+
segments << Segment.new("\n")
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Caption
|
|
253
|
+
if @caption && @show_edge
|
|
254
|
+
segments.concat(render_caption(table_width - 2))
|
|
255
|
+
segments << Segment.new("\n")
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
segments
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Render table to string
|
|
262
|
+
# @param max_width [Integer] Maximum width
|
|
263
|
+
# @param color_system [Symbol] Color system
|
|
264
|
+
# @return [String]
|
|
265
|
+
def render(max_width: 80, color_system: ColorSystem::TRUECOLOR)
|
|
266
|
+
Segment.render(to_segments(max_width: max_width), color_system: color_system)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Print table to console
|
|
270
|
+
# @param console [Console] Console to print to
|
|
271
|
+
def print_to(console)
|
|
272
|
+
rendered = render(max_width: console.width, color_system: console.color_system)
|
|
273
|
+
console.write(rendered)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
private
|
|
277
|
+
|
|
278
|
+
def calculate_column_widths(max_width)
|
|
279
|
+
available = max_width - (@columns.length + 1) - @columns.length * @padding * 2
|
|
280
|
+
|
|
281
|
+
# Calculate minimum width for each column
|
|
282
|
+
widths = @columns.each_with_index.map do |col, i|
|
|
283
|
+
header_width = Cells.cell_len(col.header)
|
|
284
|
+
max_cell = @rows.map { |r| Cells.cell_len(r[i] || "") }.max || 0
|
|
285
|
+
footer_width = col.footer ? Cells.cell_len(col.footer) : 0
|
|
286
|
+
|
|
287
|
+
min = [header_width, max_cell, footer_width].max
|
|
288
|
+
min = [min, col.min_width].max if col.min_width
|
|
289
|
+
min = [min, col.max_width].min if col.max_width
|
|
290
|
+
min
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
total = widths.sum
|
|
294
|
+
if total > available && @expand
|
|
295
|
+
# Need to shrink
|
|
296
|
+
ratio = available.to_f / total
|
|
297
|
+
widths = widths.map { |w| [(w * ratio).to_i, 3].max }
|
|
298
|
+
elsif total < available && @expand
|
|
299
|
+
# Distribute extra space
|
|
300
|
+
extra = available - total
|
|
301
|
+
per_col = extra / @columns.length
|
|
302
|
+
widths = widths.map { |w| w + per_col }
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
widths
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def render_title(width)
|
|
309
|
+
segments = []
|
|
310
|
+
title_width = Cells.cell_len(@title)
|
|
311
|
+
padding = (width - title_width) / 2
|
|
312
|
+
|
|
313
|
+
if @show_edge
|
|
314
|
+
segments << Segment.new(@box.top_left, style: @border_style)
|
|
315
|
+
segments << Segment.new(@box.horizontal * padding, style: @border_style)
|
|
316
|
+
segments << Segment.new(" #{@title} ", style: @title_style)
|
|
317
|
+
segments << Segment.new(@box.horizontal * (width - padding - title_width - 2), style: @border_style)
|
|
318
|
+
segments << Segment.new(@box.top_right, style: @border_style)
|
|
319
|
+
else
|
|
320
|
+
segments << Segment.new(" " * padding + @title, style: @title_style)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
segments
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def render_caption(width)
|
|
327
|
+
segments = []
|
|
328
|
+
caption_width = Cells.cell_len(@caption)
|
|
329
|
+
padding = (width - caption_width) / 2
|
|
330
|
+
|
|
331
|
+
segments << Segment.new(" " * padding + @caption, style: @caption_style)
|
|
332
|
+
segments
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def render_top_border(col_widths)
|
|
336
|
+
segments = []
|
|
337
|
+
segments << Segment.new(@box.top_left, style: @border_style)
|
|
338
|
+
|
|
339
|
+
col_widths.each_with_index do |w, i|
|
|
340
|
+
cell_width = w + @padding * 2
|
|
341
|
+
segments << Segment.new(@box.horizontal * cell_width, style: @border_style)
|
|
342
|
+
if i < col_widths.length - 1
|
|
343
|
+
segments << Segment.new(@box.top_t, style: @border_style)
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
segments << Segment.new(@box.top_right, style: @border_style)
|
|
348
|
+
segments
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def render_bottom_border(col_widths)
|
|
352
|
+
segments = []
|
|
353
|
+
segments << Segment.new(@box.bottom_left, style: @border_style)
|
|
354
|
+
|
|
355
|
+
col_widths.each_with_index do |w, i|
|
|
356
|
+
cell_width = w + @padding * 2
|
|
357
|
+
segments << Segment.new(@box.horizontal * cell_width, style: @border_style)
|
|
358
|
+
if i < col_widths.length - 1
|
|
359
|
+
segments << Segment.new(@box.bottom_t, style: @border_style)
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
segments << Segment.new(@box.bottom_right, style: @border_style)
|
|
364
|
+
segments
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def render_row_separator(col_widths)
|
|
368
|
+
segments = []
|
|
369
|
+
segments << Segment.new(@box.left_t, style: @border_style)
|
|
370
|
+
|
|
371
|
+
col_widths.each_with_index do |w, i|
|
|
372
|
+
cell_width = w + @padding * 2
|
|
373
|
+
segments << Segment.new(@box.horizontal * cell_width, style: @border_style)
|
|
374
|
+
if i < col_widths.length - 1
|
|
375
|
+
segments << Segment.new(@box.cross, style: @border_style)
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
segments << Segment.new(@box.right_t, style: @border_style)
|
|
380
|
+
segments
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def render_header_separator(col_widths)
|
|
384
|
+
segments = []
|
|
385
|
+
segments << Segment.new(@box.thick_left_t, style: @border_style)
|
|
386
|
+
|
|
387
|
+
col_widths.each_with_index do |w, i|
|
|
388
|
+
cell_width = w + @padding * 2
|
|
389
|
+
segments << Segment.new(@box.thick_horizontal * cell_width, style: @border_style)
|
|
390
|
+
if i < col_widths.length - 1
|
|
391
|
+
segments << Segment.new(@box.thick_cross, style: @border_style)
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
segments << Segment.new(@box.thick_right_t, style: @border_style)
|
|
396
|
+
segments
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def render_footer_separator(col_widths)
|
|
400
|
+
render_row_separator(col_widths)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def render_header_row(col_widths)
|
|
404
|
+
segments = []
|
|
405
|
+
segments << Segment.new(@box.vertical, style: @border_style)
|
|
406
|
+
|
|
407
|
+
@columns.each_with_index do |col, i|
|
|
408
|
+
width = col_widths[i]
|
|
409
|
+
content = col.header
|
|
410
|
+
cell_style = col.header_style || @header_style
|
|
411
|
+
|
|
412
|
+
segments.concat(render_cell(content, width, col.justify, cell_style))
|
|
413
|
+
|
|
414
|
+
segments << Segment.new(@box.vertical, style: @border_style)
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
segments
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def render_footer_row(col_widths)
|
|
421
|
+
segments = []
|
|
422
|
+
segments << Segment.new(@box.vertical, style: @border_style)
|
|
423
|
+
|
|
424
|
+
@columns.each_with_index do |col, i|
|
|
425
|
+
width = col_widths[i]
|
|
426
|
+
content = col.footer || ""
|
|
427
|
+
cell_style = col.footer_style
|
|
428
|
+
|
|
429
|
+
segments.concat(render_cell(content, width, col.justify, cell_style))
|
|
430
|
+
|
|
431
|
+
segments << Segment.new(@box.vertical, style: @border_style)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
segments
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def render_data_row(row, col_widths, row_index)
|
|
438
|
+
segments = []
|
|
439
|
+
row_style = nil
|
|
440
|
+
|
|
441
|
+
if @row_styles
|
|
442
|
+
styles = @row_styles.is_a?(Array) ? @row_styles : [@row_styles]
|
|
443
|
+
row_style = styles[row_index % styles.length]
|
|
444
|
+
row_style = Style.parse(row_style) if row_style.is_a?(String)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
segments << Segment.new(@box.vertical, style: @border_style)
|
|
448
|
+
|
|
449
|
+
@columns.each_with_index do |col, i|
|
|
450
|
+
width = col_widths[i]
|
|
451
|
+
content = row[i] || ""
|
|
452
|
+
cell_style = col.style || row_style
|
|
453
|
+
|
|
454
|
+
segments.concat(render_cell(content, width, col.justify, cell_style))
|
|
455
|
+
|
|
456
|
+
segments << Segment.new(@box.vertical, style: @border_style)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
segments
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def render_cell(content, width, justify, style)
|
|
463
|
+
segments = []
|
|
464
|
+
content_width = Cells.cell_len(content)
|
|
465
|
+
|
|
466
|
+
# Truncate if needed
|
|
467
|
+
if content_width > width
|
|
468
|
+
case @columns.first&.overflow || :ellipsis
|
|
469
|
+
when :ellipsis
|
|
470
|
+
content = truncate_with_ellipsis(content, width)
|
|
471
|
+
content_width = Cells.cell_len(content)
|
|
472
|
+
when :crop
|
|
473
|
+
content = truncate(content, width)
|
|
474
|
+
content_width = Cells.cell_len(content)
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Padding
|
|
479
|
+
segments << Segment.new(" " * @padding)
|
|
480
|
+
|
|
481
|
+
# Content with justification
|
|
482
|
+
padding = width - content_width
|
|
483
|
+
case justify
|
|
484
|
+
when :right
|
|
485
|
+
segments << Segment.new(" " * padding)
|
|
486
|
+
segments << Segment.new(content, style: style)
|
|
487
|
+
when :center
|
|
488
|
+
left = padding / 2
|
|
489
|
+
right = padding - left
|
|
490
|
+
segments << Segment.new(" " * left)
|
|
491
|
+
segments << Segment.new(content, style: style)
|
|
492
|
+
segments << Segment.new(" " * right)
|
|
493
|
+
else # :left
|
|
494
|
+
segments << Segment.new(content, style: style)
|
|
495
|
+
segments << Segment.new(" " * padding)
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
segments << Segment.new(" " * @padding)
|
|
499
|
+
|
|
500
|
+
segments
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def truncate(text, max_width)
|
|
504
|
+
result = +""
|
|
505
|
+
current_width = 0
|
|
506
|
+
|
|
507
|
+
text.each_char do |char|
|
|
508
|
+
char_width = Cells.char_width(char)
|
|
509
|
+
break if current_width + char_width > max_width
|
|
510
|
+
|
|
511
|
+
result << char
|
|
512
|
+
current_width += char_width
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
result
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def truncate_with_ellipsis(text, max_width)
|
|
519
|
+
return text if Cells.cell_len(text) <= max_width
|
|
520
|
+
return "…" if max_width <= 1
|
|
521
|
+
|
|
522
|
+
truncate(text, max_width - 1) + "…"
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "color_triplet"
|
|
4
|
+
require_relative "_palettes"
|
|
5
|
+
|
|
6
|
+
module Rich
|
|
7
|
+
# Terminal color theme configuration.
|
|
8
|
+
# Defines the actual RGB values for ANSI colors as rendered by a terminal.
|
|
9
|
+
class TerminalTheme
|
|
10
|
+
# @return [ColorTriplet] Default foreground color
|
|
11
|
+
attr_reader :foreground
|
|
12
|
+
|
|
13
|
+
# @return [ColorTriplet] Default background color
|
|
14
|
+
attr_reader :background
|
|
15
|
+
|
|
16
|
+
# @return [Array<ColorTriplet>] 16 ANSI colors (indices 0-15)
|
|
17
|
+
attr_reader :ansi_colors
|
|
18
|
+
|
|
19
|
+
# Create a new terminal theme
|
|
20
|
+
# @param foreground [ColorTriplet] Default foreground color
|
|
21
|
+
# @param background [ColorTriplet] Default background color
|
|
22
|
+
# @param ansi_colors [Array<ColorTriplet>] 16 ANSI colors
|
|
23
|
+
def initialize(foreground:, background:, ansi_colors:)
|
|
24
|
+
raise ArgumentError, "ansi_colors must have exactly 16 colors" unless ansi_colors.length == 16
|
|
25
|
+
|
|
26
|
+
@foreground = foreground
|
|
27
|
+
@background = background
|
|
28
|
+
@ansi_colors = ansi_colors.freeze
|
|
29
|
+
freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Check equality with another theme
|
|
33
|
+
def ==(other)
|
|
34
|
+
return false unless other.is_a?(TerminalTheme)
|
|
35
|
+
|
|
36
|
+
@foreground == other.foreground &&
|
|
37
|
+
@background == other.background &&
|
|
38
|
+
@ansi_colors == other.ansi_colors
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
alias eql? ==
|
|
42
|
+
|
|
43
|
+
def hash
|
|
44
|
+
[@foreground, @background, @ansi_colors].hash
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Default terminal theme (based on typical dark terminal)
|
|
49
|
+
DEFAULT_TERMINAL_THEME = TerminalTheme.new(
|
|
50
|
+
foreground: ColorTriplet.new(230, 230, 230),
|
|
51
|
+
background: ColorTriplet.new(12, 12, 12),
|
|
52
|
+
ansi_colors: [
|
|
53
|
+
ColorTriplet.new(12, 12, 12), # 0: Black
|
|
54
|
+
ColorTriplet.new(205, 49, 49), # 1: Red
|
|
55
|
+
ColorTriplet.new(13, 188, 121), # 2: Green
|
|
56
|
+
ColorTriplet.new(229, 229, 16), # 3: Yellow
|
|
57
|
+
ColorTriplet.new(36, 114, 200), # 4: Blue
|
|
58
|
+
ColorTriplet.new(188, 63, 188), # 5: Magenta
|
|
59
|
+
ColorTriplet.new(17, 168, 205), # 6: Cyan
|
|
60
|
+
ColorTriplet.new(229, 229, 229), # 7: White
|
|
61
|
+
ColorTriplet.new(102, 102, 102), # 8: Bright Black
|
|
62
|
+
ColorTriplet.new(241, 76, 76), # 9: Bright Red
|
|
63
|
+
ColorTriplet.new(35, 209, 139), # 10: Bright Green
|
|
64
|
+
ColorTriplet.new(245, 245, 67), # 11: Bright Yellow
|
|
65
|
+
ColorTriplet.new(59, 142, 234), # 12: Bright Blue
|
|
66
|
+
ColorTriplet.new(214, 112, 214), # 13: Bright Magenta
|
|
67
|
+
ColorTriplet.new(41, 184, 219), # 14: Bright Cyan
|
|
68
|
+
ColorTriplet.new(255, 255, 255) # 15: Bright White
|
|
69
|
+
]
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Monokai-inspired theme
|
|
73
|
+
MONOKAI_THEME = TerminalTheme.new(
|
|
74
|
+
foreground: ColorTriplet.new(248, 248, 242),
|
|
75
|
+
background: ColorTriplet.new(39, 40, 34),
|
|
76
|
+
ansi_colors: [
|
|
77
|
+
ColorTriplet.new(39, 40, 34), # 0: Black
|
|
78
|
+
ColorTriplet.new(249, 38, 114), # 1: Red
|
|
79
|
+
ColorTriplet.new(166, 226, 46), # 2: Green
|
|
80
|
+
ColorTriplet.new(244, 191, 117), # 3: Yellow
|
|
81
|
+
ColorTriplet.new(102, 217, 239), # 4: Blue
|
|
82
|
+
ColorTriplet.new(174, 129, 255), # 5: Magenta
|
|
83
|
+
ColorTriplet.new(161, 239, 228), # 6: Cyan
|
|
84
|
+
ColorTriplet.new(248, 248, 242), # 7: White
|
|
85
|
+
ColorTriplet.new(117, 113, 94), # 8: Bright Black
|
|
86
|
+
ColorTriplet.new(249, 38, 114), # 9: Bright Red
|
|
87
|
+
ColorTriplet.new(166, 226, 46), # 10: Bright Green
|
|
88
|
+
ColorTriplet.new(244, 191, 117), # 11: Bright Yellow
|
|
89
|
+
ColorTriplet.new(102, 217, 239), # 12: Bright Blue
|
|
90
|
+
ColorTriplet.new(174, 129, 255), # 13: Bright Magenta
|
|
91
|
+
ColorTriplet.new(161, 239, 228), # 14: Bright Cyan
|
|
92
|
+
ColorTriplet.new(248, 248, 242) # 15: Bright White
|
|
93
|
+
]
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Dimmed Monokai for SVG/HTML export
|
|
97
|
+
SVG_EXPORT_THEME = TerminalTheme.new(
|
|
98
|
+
foreground: ColorTriplet.new(248, 248, 242),
|
|
99
|
+
background: ColorTriplet.new(50, 48, 47),
|
|
100
|
+
ansi_colors: [
|
|
101
|
+
ColorTriplet.new(50, 48, 47), # 0: Black
|
|
102
|
+
ColorTriplet.new(255, 98, 134), # 1: Red
|
|
103
|
+
ColorTriplet.new(164, 238, 92), # 2: Green
|
|
104
|
+
ColorTriplet.new(255, 216, 102), # 3: Yellow
|
|
105
|
+
ColorTriplet.new(98, 209, 255), # 4: Blue
|
|
106
|
+
ColorTriplet.new(189, 147, 249), # 5: Magenta
|
|
107
|
+
ColorTriplet.new(128, 255, 234), # 6: Cyan
|
|
108
|
+
ColorTriplet.new(248, 248, 242), # 7: White
|
|
109
|
+
ColorTriplet.new(98, 94, 76), # 8: Bright Black
|
|
110
|
+
ColorTriplet.new(255, 98, 134), # 9: Bright Red
|
|
111
|
+
ColorTriplet.new(164, 238, 92), # 10: Bright Green
|
|
112
|
+
ColorTriplet.new(255, 216, 102), # 11: Bright Yellow
|
|
113
|
+
ColorTriplet.new(98, 209, 255), # 12: Bright Blue
|
|
114
|
+
ColorTriplet.new(189, 147, 249), # 13: Bright Magenta
|
|
115
|
+
ColorTriplet.new(128, 255, 234), # 14: Bright Cyan
|
|
116
|
+
ColorTriplet.new(248, 248, 242) # 15: Bright White
|
|
117
|
+
]
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Windows Terminal default theme
|
|
121
|
+
WINDOWS_TERMINAL_THEME = TerminalTheme.new(
|
|
122
|
+
foreground: ColorTriplet.new(204, 204, 204),
|
|
123
|
+
background: ColorTriplet.new(12, 12, 12),
|
|
124
|
+
ansi_colors: Palettes::WINDOWS_PALETTE.dup
|
|
125
|
+
)
|
|
126
|
+
end
|