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/console.rb
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "color"
|
|
4
|
+
require_relative "style"
|
|
5
|
+
require_relative "segment"
|
|
6
|
+
require_relative "control"
|
|
7
|
+
require_relative "cells"
|
|
8
|
+
require_relative "terminal_theme"
|
|
9
|
+
require_relative "win32_console" if Gem.win_platform?
|
|
10
|
+
|
|
11
|
+
module Rich
|
|
12
|
+
# Console rendering options
|
|
13
|
+
class ConsoleOptions
|
|
14
|
+
# @return [Integer] Minimum width for rendering
|
|
15
|
+
attr_reader :min_width
|
|
16
|
+
|
|
17
|
+
# @return [Integer] Maximum width for rendering
|
|
18
|
+
attr_reader :max_width
|
|
19
|
+
|
|
20
|
+
# @return [Integer, nil] Height for rendering
|
|
21
|
+
attr_reader :height
|
|
22
|
+
|
|
23
|
+
# @return [Boolean] Legacy Windows console mode
|
|
24
|
+
attr_reader :legacy_windows
|
|
25
|
+
|
|
26
|
+
# @return [String] Output encoding
|
|
27
|
+
attr_reader :encoding
|
|
28
|
+
|
|
29
|
+
# @return [Boolean] Terminal output
|
|
30
|
+
attr_reader :is_terminal
|
|
31
|
+
|
|
32
|
+
# @return [Boolean] Enable highlighting
|
|
33
|
+
attr_reader :highlight
|
|
34
|
+
|
|
35
|
+
# @return [Boolean] Enable markup
|
|
36
|
+
attr_reader :markup
|
|
37
|
+
|
|
38
|
+
# @return [Boolean] No wrapping
|
|
39
|
+
attr_reader :no_wrap
|
|
40
|
+
|
|
41
|
+
def initialize(
|
|
42
|
+
min_width: 1,
|
|
43
|
+
max_width: 80,
|
|
44
|
+
height: nil,
|
|
45
|
+
legacy_windows: false,
|
|
46
|
+
encoding: "utf-8",
|
|
47
|
+
is_terminal: true,
|
|
48
|
+
highlight: true,
|
|
49
|
+
markup: true,
|
|
50
|
+
no_wrap: false
|
|
51
|
+
)
|
|
52
|
+
@min_width = min_width
|
|
53
|
+
@max_width = max_width
|
|
54
|
+
@height = height
|
|
55
|
+
@legacy_windows = legacy_windows
|
|
56
|
+
@encoding = encoding
|
|
57
|
+
@is_terminal = is_terminal
|
|
58
|
+
@highlight = highlight
|
|
59
|
+
@markup = markup
|
|
60
|
+
@no_wrap = no_wrap
|
|
61
|
+
freeze
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Update options, returning a new instance
|
|
65
|
+
# @return [ConsoleOptions]
|
|
66
|
+
def update(**kwargs)
|
|
67
|
+
ConsoleOptions.new(
|
|
68
|
+
min_width: kwargs.fetch(:min_width, @min_width),
|
|
69
|
+
max_width: kwargs.fetch(:max_width, @max_width),
|
|
70
|
+
height: kwargs.fetch(:height, @height),
|
|
71
|
+
legacy_windows: kwargs.fetch(:legacy_windows, @legacy_windows),
|
|
72
|
+
encoding: kwargs.fetch(:encoding, @encoding),
|
|
73
|
+
is_terminal: kwargs.fetch(:is_terminal, @is_terminal),
|
|
74
|
+
highlight: kwargs.fetch(:highlight, @highlight),
|
|
75
|
+
markup: kwargs.fetch(:markup, @markup),
|
|
76
|
+
no_wrap: kwargs.fetch(:no_wrap, @no_wrap)
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Update width
|
|
81
|
+
# @param width [Integer] New width
|
|
82
|
+
# @return [ConsoleOptions]
|
|
83
|
+
def update_width(width)
|
|
84
|
+
update(min_width: width, max_width: width)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Main console class for terminal output
|
|
89
|
+
class Console
|
|
90
|
+
# @return [IO] Output file
|
|
91
|
+
attr_reader :file
|
|
92
|
+
|
|
93
|
+
# @return [Symbol] Color system
|
|
94
|
+
attr_reader :color_system
|
|
95
|
+
|
|
96
|
+
# @return [Boolean] Force terminal mode
|
|
97
|
+
attr_reader :force_terminal
|
|
98
|
+
|
|
99
|
+
# @return [Boolean] Enable markup
|
|
100
|
+
attr_reader :markup
|
|
101
|
+
|
|
102
|
+
# @return [Boolean] Enable highlighting
|
|
103
|
+
attr_reader :highlight
|
|
104
|
+
|
|
105
|
+
# @return [Integer, nil] Override width
|
|
106
|
+
attr_reader :width_override
|
|
107
|
+
|
|
108
|
+
# @return [Integer, nil] Override height
|
|
109
|
+
attr_reader :height_override
|
|
110
|
+
|
|
111
|
+
# @return [Style, nil] Default style
|
|
112
|
+
attr_reader :style
|
|
113
|
+
|
|
114
|
+
# @return [Boolean] Safe output (escape HTML)
|
|
115
|
+
attr_reader :safe_box
|
|
116
|
+
|
|
117
|
+
# @return [TerminalTheme] Terminal theme
|
|
118
|
+
attr_reader :theme
|
|
119
|
+
|
|
120
|
+
# Default refresh rate for progress/live (Hz)
|
|
121
|
+
DEFAULT_REFRESH_RATE = Gem.win_platform? ? 5 : 10
|
|
122
|
+
|
|
123
|
+
def initialize(
|
|
124
|
+
file: $stdout,
|
|
125
|
+
color_system: nil,
|
|
126
|
+
force_terminal: nil,
|
|
127
|
+
markup: true,
|
|
128
|
+
highlight: true,
|
|
129
|
+
width: nil,
|
|
130
|
+
height: nil,
|
|
131
|
+
style: nil,
|
|
132
|
+
safe_box: true,
|
|
133
|
+
theme: nil
|
|
134
|
+
)
|
|
135
|
+
@file = file
|
|
136
|
+
@force_terminal = force_terminal
|
|
137
|
+
@markup = markup
|
|
138
|
+
@highlight = highlight
|
|
139
|
+
@width_override = width
|
|
140
|
+
@height_override = height
|
|
141
|
+
@style = style.is_a?(String) ? Style.parse(style) : style
|
|
142
|
+
@safe_box = safe_box
|
|
143
|
+
@theme = theme || DEFAULT_TERMINAL_THEME
|
|
144
|
+
|
|
145
|
+
@color_system = color_system || detect_color_system
|
|
146
|
+
@legacy_windows = detect_legacy_windows
|
|
147
|
+
|
|
148
|
+
# Enable ANSI on Windows if possible
|
|
149
|
+
if Gem.win_platform? && defined?(Win32Console)
|
|
150
|
+
Win32Console.enable_ansi!
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# @return [Boolean] Whether output is a terminal
|
|
155
|
+
def is_terminal?
|
|
156
|
+
return @force_terminal unless @force_terminal.nil?
|
|
157
|
+
@file.respond_to?(:tty?) && @file.tty?
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# @return [Boolean] Is output a terminal
|
|
161
|
+
def terminal?
|
|
162
|
+
return @force_terminal unless @force_terminal.nil?
|
|
163
|
+
return false unless @file.respond_to?(:tty?)
|
|
164
|
+
|
|
165
|
+
@file.tty?
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# @return [Boolean] Is this a legacy Windows console
|
|
169
|
+
def legacy_windows?
|
|
170
|
+
@legacy_windows
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# @return [Integer] Console width in characters
|
|
174
|
+
def width
|
|
175
|
+
return @width_override if @width_override
|
|
176
|
+
|
|
177
|
+
detect_size[0]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# @return [Integer] Console height in characters
|
|
181
|
+
def height
|
|
182
|
+
return @height_override if @height_override
|
|
183
|
+
|
|
184
|
+
detect_size[1]
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# @return [Array<Integer>] [width, height]
|
|
188
|
+
def size
|
|
189
|
+
[width, height]
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Get console options for rendering
|
|
193
|
+
# @return [ConsoleOptions]
|
|
194
|
+
def options
|
|
195
|
+
ConsoleOptions.new(
|
|
196
|
+
min_width: 1,
|
|
197
|
+
max_width: width,
|
|
198
|
+
height: height,
|
|
199
|
+
legacy_windows: @legacy_windows,
|
|
200
|
+
encoding: encoding,
|
|
201
|
+
is_terminal: terminal?,
|
|
202
|
+
highlight: @highlight,
|
|
203
|
+
markup: @markup
|
|
204
|
+
)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# @return [String] Output encoding
|
|
208
|
+
def encoding
|
|
209
|
+
@file.respond_to?(:encoding) ? @file.encoding.to_s : "utf-8"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Print objects to the console
|
|
213
|
+
# @param objects [Array] Objects to print
|
|
214
|
+
# @param sep [String] Separator
|
|
215
|
+
# @param end_str [String] End string
|
|
216
|
+
# @param style [String, Style, nil] Style
|
|
217
|
+
# @param highlight [Boolean] Enable highlighting
|
|
218
|
+
# @return [void]
|
|
219
|
+
def print(*objects, sep: " ", end_str: "\n", style: nil, highlight: nil)
|
|
220
|
+
highlight = @highlight if highlight.nil?
|
|
221
|
+
|
|
222
|
+
text = objects.map do |obj|
|
|
223
|
+
render_object(obj, highlight: highlight)
|
|
224
|
+
end.join(sep)
|
|
225
|
+
|
|
226
|
+
text += end_str
|
|
227
|
+
|
|
228
|
+
if style
|
|
229
|
+
applied_style = style.is_a?(String) ? Style.parse(style) : style
|
|
230
|
+
write_styled(text, applied_style)
|
|
231
|
+
else
|
|
232
|
+
write(text)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Print with markup parsing
|
|
237
|
+
# @param text [String] Text with markup
|
|
238
|
+
# @return [void]
|
|
239
|
+
def print_markup(text)
|
|
240
|
+
segments = parse_markup(text)
|
|
241
|
+
write_segments(segments)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Print JSON with highlighting
|
|
245
|
+
# @param json [String, nil] JSON string
|
|
246
|
+
# @param data [Object] Data to convert
|
|
247
|
+
# @param indent [Integer] Indentation
|
|
248
|
+
# @return [void]
|
|
249
|
+
def print_json(json = nil, data: nil, indent: 2)
|
|
250
|
+
require "json"
|
|
251
|
+
|
|
252
|
+
json_str = json || JSON.pretty_generate(data, indent: " " * indent)
|
|
253
|
+
|
|
254
|
+
if @highlight
|
|
255
|
+
# Colorize JSON
|
|
256
|
+
highlighted = colorize_json(json_str)
|
|
257
|
+
print(highlighted)
|
|
258
|
+
else
|
|
259
|
+
print(json_str)
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Print a horizontal rule
|
|
264
|
+
# @param title [String] Title
|
|
265
|
+
# @param style [String] Style
|
|
266
|
+
# @return [void]
|
|
267
|
+
def rule(title = "", style: "rule.line")
|
|
268
|
+
console_width = width
|
|
269
|
+
rule_style = Style.parse(style)
|
|
270
|
+
|
|
271
|
+
if title.empty?
|
|
272
|
+
line = "─" * console_width
|
|
273
|
+
write_styled(line + "\n", rule_style)
|
|
274
|
+
else
|
|
275
|
+
title_length = Cells.cell_len(title) + 2
|
|
276
|
+
remaining = console_width - title_length
|
|
277
|
+
|
|
278
|
+
if remaining > 0
|
|
279
|
+
left_width = remaining / 2
|
|
280
|
+
right_width = remaining - left_width
|
|
281
|
+
line = "─" * left_width + " #{title} " + "─" * right_width
|
|
282
|
+
else
|
|
283
|
+
line = title
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
write_styled(line + "\n", rule_style)
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Clear the screen
|
|
291
|
+
# @return [void]
|
|
292
|
+
def clear
|
|
293
|
+
if @legacy_windows && defined?(Win32Console)
|
|
294
|
+
Win32Console.clear_screen
|
|
295
|
+
else
|
|
296
|
+
write(Control.clear_screen)
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Show the cursor
|
|
301
|
+
# @return [void]
|
|
302
|
+
def show_cursor
|
|
303
|
+
if @legacy_windows && defined?(Win32Console)
|
|
304
|
+
Win32Console.show_cursor
|
|
305
|
+
else
|
|
306
|
+
write(Control.show_cursor)
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Hide the cursor
|
|
311
|
+
# @return [void]
|
|
312
|
+
def hide_cursor
|
|
313
|
+
if @legacy_windows && defined?(Win32Console)
|
|
314
|
+
Win32Console.hide_cursor
|
|
315
|
+
else
|
|
316
|
+
write(Control.hide_cursor)
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Set window title
|
|
321
|
+
# @param title [String] Window title
|
|
322
|
+
# @return [void]
|
|
323
|
+
def set_title(title)
|
|
324
|
+
if @legacy_windows && defined?(Win32Console)
|
|
325
|
+
Win32Console.set_title(title)
|
|
326
|
+
else
|
|
327
|
+
write(Control.set_title(title))
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Write raw text
|
|
332
|
+
# @param text [String] Text to write
|
|
333
|
+
# @return [void]
|
|
334
|
+
def write(text)
|
|
335
|
+
@file.write(text)
|
|
336
|
+
@file.flush if @file.respond_to?(:flush)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Write styled text
|
|
340
|
+
# @param text [String] Text to write
|
|
341
|
+
# @param style [Style] Style to apply
|
|
342
|
+
# @return [void]
|
|
343
|
+
def write_styled(text, style)
|
|
344
|
+
if @legacy_windows && defined?(Win32Console)
|
|
345
|
+
write_styled_legacy(text, style)
|
|
346
|
+
else
|
|
347
|
+
rendered = style.render(color_system: @color_system)
|
|
348
|
+
write("#{rendered}#{text}\e[0m")
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Write segments to output
|
|
353
|
+
# @param segments [Array<Segment>] Segments to write
|
|
354
|
+
# @return [void]
|
|
355
|
+
def write_segments(segments)
|
|
356
|
+
rendered = Segment.render(segments, color_system: @color_system)
|
|
357
|
+
write(rendered)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Inspect an object
|
|
361
|
+
# @param obj [Object] Object to inspect
|
|
362
|
+
# @param title [String, nil] Title
|
|
363
|
+
# @param methods [Boolean] Show methods
|
|
364
|
+
# @param docs [Boolean] Show docs
|
|
365
|
+
# @return [void]
|
|
366
|
+
def inspect(obj, title: nil, methods: false, docs: true)
|
|
367
|
+
title ||= obj.class.name
|
|
368
|
+
|
|
369
|
+
rule(title, style: "bold")
|
|
370
|
+
|
|
371
|
+
print("Class: #{obj.class}")
|
|
372
|
+
print("Object ID: #{obj.object_id}")
|
|
373
|
+
|
|
374
|
+
if obj.respond_to?(:instance_variables)
|
|
375
|
+
ivars = obj.instance_variables
|
|
376
|
+
unless ivars.empty?
|
|
377
|
+
print("\nInstance Variables:")
|
|
378
|
+
ivars.each do |ivar|
|
|
379
|
+
value = obj.instance_variable_get(ivar)
|
|
380
|
+
print(" #{ivar}: #{value.inspect}")
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
if methods && obj.respond_to?(:methods)
|
|
386
|
+
obj_methods = (obj.methods - Object.methods).sort
|
|
387
|
+
unless obj_methods.empty?
|
|
388
|
+
print("\nMethods:")
|
|
389
|
+
obj_methods.each do |method|
|
|
390
|
+
print(" #{method}")
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
rule(style: "bold")
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
private
|
|
399
|
+
|
|
400
|
+
def detect_color_system
|
|
401
|
+
return ColorSystem::WINDOWS if Gem.win_platform? && !ansi_supported?
|
|
402
|
+
|
|
403
|
+
# Check terminal capabilities
|
|
404
|
+
term = ENV["TERM"] || ""
|
|
405
|
+
colorterm = ENV["COLORTERM"] || ""
|
|
406
|
+
|
|
407
|
+
if colorterm.downcase.include?("truecolor") || colorterm.downcase.include?("24bit")
|
|
408
|
+
return ColorSystem::TRUECOLOR
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
if term.include?("256color") || ENV["TERM_PROGRAM"] == "iTerm.app"
|
|
412
|
+
return ColorSystem::EIGHT_BIT
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
if %w[xterm vt100 screen].any? { |t| term.include?(t) }
|
|
416
|
+
return ColorSystem::STANDARD
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# Default to truecolor for modern terminals
|
|
420
|
+
ColorSystem::TRUECOLOR
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def detect_legacy_windows
|
|
424
|
+
return false unless Gem.win_platform?
|
|
425
|
+
return false if ansi_supported?
|
|
426
|
+
|
|
427
|
+
true
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def ansi_supported?
|
|
431
|
+
return true unless Gem.win_platform?
|
|
432
|
+
return true unless defined?(Win32Console)
|
|
433
|
+
|
|
434
|
+
Win32Console.supports_ansi?
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def detect_size
|
|
438
|
+
# Try Ruby's built-in IO#winsize
|
|
439
|
+
if @file.respond_to?(:winsize)
|
|
440
|
+
begin
|
|
441
|
+
height, width = @file.winsize
|
|
442
|
+
return [width, height] if width > 0 && height > 0
|
|
443
|
+
rescue StandardError
|
|
444
|
+
# Fall through
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Try Windows API
|
|
449
|
+
if Gem.win_platform? && defined?(Win32Console)
|
|
450
|
+
size = Win32Console.get_size
|
|
451
|
+
return size if size
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
# Try environment variables
|
|
455
|
+
cols = ENV["COLUMNS"]&.to_i
|
|
456
|
+
rows = ENV["LINES"]&.to_i
|
|
457
|
+
return [cols, rows] if cols && cols > 0 && rows && rows > 0
|
|
458
|
+
|
|
459
|
+
# Default
|
|
460
|
+
[80, 24]
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def render_object(obj, highlight: true)
|
|
464
|
+
case obj
|
|
465
|
+
when String
|
|
466
|
+
if @markup
|
|
467
|
+
render_markup(obj)
|
|
468
|
+
else
|
|
469
|
+
obj
|
|
470
|
+
end
|
|
471
|
+
when Segment
|
|
472
|
+
obj.text
|
|
473
|
+
else
|
|
474
|
+
obj.to_s
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def render_markup(text)
|
|
479
|
+
# Simple markup rendering - just remove tags for now
|
|
480
|
+
# Full implementation in markup.rb
|
|
481
|
+
text.gsub(/\[\/?\w+[^\]]*\]/, "")
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def parse_markup(text)
|
|
485
|
+
# Simple markup parser - converts [style]text[/style] to segments
|
|
486
|
+
segments = []
|
|
487
|
+
style_stack = []
|
|
488
|
+
pos = 0
|
|
489
|
+
|
|
490
|
+
tag_regex = /\[(?<close>\/)?(?<style>[^\]]+)\]/
|
|
491
|
+
|
|
492
|
+
text.scan(tag_regex) do
|
|
493
|
+
match = Regexp.last_match
|
|
494
|
+
match_start = match.begin(0)
|
|
495
|
+
|
|
496
|
+
# Add text before tag
|
|
497
|
+
if match_start > pos
|
|
498
|
+
current_style = style_stack.empty? ? nil : style_stack.reduce { |a, b| a + b }
|
|
499
|
+
segments << Segment.new(text[pos...match_start], style: current_style)
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
if match[:close]
|
|
503
|
+
style_stack.pop
|
|
504
|
+
else
|
|
505
|
+
parsed_style = Style.parse(match[:style])
|
|
506
|
+
style_stack.push(parsed_style)
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
pos = match.end(0)
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
# Add remaining text
|
|
513
|
+
if pos < text.length
|
|
514
|
+
current_style = style_stack.empty? ? nil : style_stack.reduce { |a, b| a + b }
|
|
515
|
+
segments << Segment.new(text[pos..], style: current_style)
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
segments
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def colorize_json(json_str)
|
|
522
|
+
# Simple JSON colorization
|
|
523
|
+
json_str
|
|
524
|
+
.gsub(/"([^"]+)"(?=\s*:)/) { "[cyan]\"#{Regexp.last_match(1)}\"[/]" } # Keys
|
|
525
|
+
.gsub(/:\s*"([^"]*)"/) { ": [green]\"#{Regexp.last_match(1)}\"[/]" } # String values
|
|
526
|
+
.gsub(/:\s*(\d+\.?\d*)/) { ": [yellow]#{Regexp.last_match(1)}[/]" } # Numbers
|
|
527
|
+
.gsub(/:\s*(true|false)/) { ": [italic]#{Regexp.last_match(1)}[/]" } # Booleans
|
|
528
|
+
.gsub(/:\s*null/) { ": [dim]null[/]" } # Null
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def write_styled_legacy(text, style)
|
|
532
|
+
return write(text) unless defined?(Win32Console)
|
|
533
|
+
|
|
534
|
+
# Map style to Windows console attributes
|
|
535
|
+
fg = style.color&.number || 7
|
|
536
|
+
bg = style.bgcolor&.number || 0
|
|
537
|
+
|
|
538
|
+
# Apply bold as bright
|
|
539
|
+
fg |= 8 if style.bold?
|
|
540
|
+
|
|
541
|
+
attributes = Win32Console.ansi_to_windows_attributes(foreground: fg, background: bg)
|
|
542
|
+
original_attrs = Win32Console.get_text_attributes
|
|
543
|
+
|
|
544
|
+
Win32Console.set_text_attribute(attributes)
|
|
545
|
+
write(text)
|
|
546
|
+
Win32Console.set_text_attribute(original_attrs) if original_attrs
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
end
|