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.
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