rich-ruby 1.0.1 → 1.0.2
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 +80 -0
- data/LICENSE +21 -21
- data/README.md +547 -547
- data/docs/architecture.md +43 -0
- data/docs/cheat-sheet.md +52 -0
- data/docs/customization.md +53 -0
- data/docs/how-to-use.md +96 -0
- data/docs/test-report.md +112 -0
- data/docs/troubleshooting.md +36 -0
- data/docs/windows-notes.md +30 -0
- data/examples/demo.rb +106 -106
- data/examples/showcase.rb +420 -420
- data/examples/smoke_test.rb +41 -41
- data/examples/stress_test.rb +604 -604
- data/examples/syntax_markdown_demo.rb +166 -166
- data/examples/verify.rb +216 -215
- data/examples/visual_demo.rb +145 -145
- data/lib/rich/_palettes.rb +148 -148
- data/lib/rich/box.rb +342 -342
- data/lib/rich/cells.rb +524 -512
- data/lib/rich/color.rb +631 -628
- data/lib/rich/color_triplet.rb +227 -220
- data/lib/rich/console.rb +604 -549
- data/lib/rich/control.rb +332 -332
- data/lib/rich/json.rb +260 -254
- data/lib/rich/layout.rb +314 -314
- data/lib/rich/markdown.rb +531 -509
- data/lib/rich/markup.rb +186 -175
- data/lib/rich/panel.rb +318 -311
- data/lib/rich/progress.rb +430 -430
- data/lib/rich/segment.rb +387 -387
- data/lib/rich/style.rb +464 -433
- data/lib/rich/syntax.rb +1220 -1145
- data/lib/rich/table.rb +547 -525
- data/lib/rich/terminal_theme.rb +126 -126
- data/lib/rich/text.rb +460 -433
- data/lib/rich/tree.rb +220 -220
- data/lib/rich/version.rb +5 -5
- data/lib/rich/win32_console.rb +620 -582
- data/lib/rich.rb +108 -108
- metadata +15 -5
data/lib/rich/console.rb
CHANGED
|
@@ -1,549 +1,604 @@
|
|
|
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 "
|
|
9
|
-
require_relative "
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
@
|
|
55
|
-
@
|
|
56
|
-
@
|
|
57
|
-
@
|
|
58
|
-
@
|
|
59
|
-
@
|
|
60
|
-
@
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
@
|
|
138
|
-
@
|
|
139
|
-
@
|
|
140
|
-
@
|
|
141
|
-
@
|
|
142
|
-
@
|
|
143
|
-
@
|
|
144
|
-
|
|
145
|
-
@
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
@
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
# @
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
write_segments(segments)
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
# Print
|
|
245
|
-
# @param
|
|
246
|
-
# @
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
#
|
|
340
|
-
# @
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
#
|
|
362
|
-
# @param
|
|
363
|
-
# @
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
return
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
if
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
end
|
|
548
|
-
|
|
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 "text"
|
|
9
|
+
require_relative "markup"
|
|
10
|
+
require_relative "terminal_theme"
|
|
11
|
+
require_relative "win32_console" if Gem.win_platform?
|
|
12
|
+
|
|
13
|
+
module Rich
|
|
14
|
+
# Console rendering options
|
|
15
|
+
class ConsoleOptions
|
|
16
|
+
# @return [Integer] Minimum width for rendering
|
|
17
|
+
attr_reader :min_width
|
|
18
|
+
|
|
19
|
+
# @return [Integer] Maximum width for rendering
|
|
20
|
+
attr_reader :max_width
|
|
21
|
+
|
|
22
|
+
# @return [Integer, nil] Height for rendering
|
|
23
|
+
attr_reader :height
|
|
24
|
+
|
|
25
|
+
# @return [Boolean] Legacy Windows console mode
|
|
26
|
+
attr_reader :legacy_windows
|
|
27
|
+
|
|
28
|
+
# @return [String] Output encoding
|
|
29
|
+
attr_reader :encoding
|
|
30
|
+
|
|
31
|
+
# @return [Boolean] Terminal output
|
|
32
|
+
attr_reader :is_terminal
|
|
33
|
+
|
|
34
|
+
# @return [Boolean] Enable highlighting
|
|
35
|
+
attr_reader :highlight
|
|
36
|
+
|
|
37
|
+
# @return [Boolean] Enable markup
|
|
38
|
+
attr_reader :markup
|
|
39
|
+
|
|
40
|
+
# @return [Boolean] No wrapping
|
|
41
|
+
attr_reader :no_wrap
|
|
42
|
+
|
|
43
|
+
def initialize(
|
|
44
|
+
min_width: 1,
|
|
45
|
+
max_width: 80,
|
|
46
|
+
height: nil,
|
|
47
|
+
legacy_windows: false,
|
|
48
|
+
encoding: "utf-8",
|
|
49
|
+
is_terminal: true,
|
|
50
|
+
highlight: true,
|
|
51
|
+
markup: true,
|
|
52
|
+
no_wrap: false
|
|
53
|
+
)
|
|
54
|
+
@min_width = min_width
|
|
55
|
+
@max_width = max_width
|
|
56
|
+
@height = height
|
|
57
|
+
@legacy_windows = legacy_windows
|
|
58
|
+
@encoding = encoding
|
|
59
|
+
@is_terminal = is_terminal
|
|
60
|
+
@highlight = highlight
|
|
61
|
+
@markup = markup
|
|
62
|
+
@no_wrap = no_wrap
|
|
63
|
+
freeze
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Update options, returning a new instance
|
|
67
|
+
# @return [ConsoleOptions]
|
|
68
|
+
def update(**kwargs)
|
|
69
|
+
ConsoleOptions.new(
|
|
70
|
+
min_width: kwargs.fetch(:min_width, @min_width),
|
|
71
|
+
max_width: kwargs.fetch(:max_width, @max_width),
|
|
72
|
+
height: kwargs.fetch(:height, @height),
|
|
73
|
+
legacy_windows: kwargs.fetch(:legacy_windows, @legacy_windows),
|
|
74
|
+
encoding: kwargs.fetch(:encoding, @encoding),
|
|
75
|
+
is_terminal: kwargs.fetch(:is_terminal, @is_terminal),
|
|
76
|
+
highlight: kwargs.fetch(:highlight, @highlight),
|
|
77
|
+
markup: kwargs.fetch(:markup, @markup),
|
|
78
|
+
no_wrap: kwargs.fetch(:no_wrap, @no_wrap)
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Update width
|
|
83
|
+
# @param width [Integer] New width
|
|
84
|
+
# @return [ConsoleOptions]
|
|
85
|
+
def update_width(width)
|
|
86
|
+
update(min_width: width, max_width: width)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Main console class for terminal output
|
|
91
|
+
class Console
|
|
92
|
+
# @return [IO] Output file
|
|
93
|
+
attr_reader :file
|
|
94
|
+
|
|
95
|
+
# @return [Symbol] Color system
|
|
96
|
+
attr_reader :color_system
|
|
97
|
+
|
|
98
|
+
# @return [Boolean] Force terminal mode
|
|
99
|
+
attr_reader :force_terminal
|
|
100
|
+
|
|
101
|
+
# @return [Boolean] Enable markup
|
|
102
|
+
attr_reader :markup
|
|
103
|
+
|
|
104
|
+
# @return [Boolean] Enable highlighting
|
|
105
|
+
attr_reader :highlight
|
|
106
|
+
|
|
107
|
+
# @return [Integer, nil] Override width
|
|
108
|
+
attr_reader :width_override
|
|
109
|
+
|
|
110
|
+
# @return [Integer, nil] Override height
|
|
111
|
+
attr_reader :height_override
|
|
112
|
+
|
|
113
|
+
# @return [Style, nil] Default style
|
|
114
|
+
attr_reader :style
|
|
115
|
+
|
|
116
|
+
# @return [Boolean] Safe output (escape HTML)
|
|
117
|
+
attr_reader :safe_box
|
|
118
|
+
|
|
119
|
+
# @return [TerminalTheme] Terminal theme
|
|
120
|
+
attr_reader :theme
|
|
121
|
+
|
|
122
|
+
# Default refresh rate for progress/live (Hz)
|
|
123
|
+
DEFAULT_REFRESH_RATE = Gem.win_platform? ? 5 : 10
|
|
124
|
+
|
|
125
|
+
def initialize(
|
|
126
|
+
file: $stdout,
|
|
127
|
+
color_system: nil,
|
|
128
|
+
force_terminal: nil,
|
|
129
|
+
markup: true,
|
|
130
|
+
highlight: true,
|
|
131
|
+
width: nil,
|
|
132
|
+
height: nil,
|
|
133
|
+
style: nil,
|
|
134
|
+
safe_box: true,
|
|
135
|
+
theme: nil
|
|
136
|
+
)
|
|
137
|
+
@file = file
|
|
138
|
+
@force_terminal = force_terminal
|
|
139
|
+
@markup = markup
|
|
140
|
+
@highlight = highlight
|
|
141
|
+
@width_override = width
|
|
142
|
+
@height_override = height
|
|
143
|
+
@style = style.is_a?(String) ? Style.parse(style) : style
|
|
144
|
+
@safe_box = safe_box
|
|
145
|
+
@theme = theme || DEFAULT_TERMINAL_THEME
|
|
146
|
+
|
|
147
|
+
@color_system = color_system || detect_color_system
|
|
148
|
+
@legacy_windows = detect_legacy_windows
|
|
149
|
+
@no_color = compute_no_color
|
|
150
|
+
|
|
151
|
+
# Enable ANSI on Windows if possible
|
|
152
|
+
if Gem.win_platform? && defined?(Win32Console)
|
|
153
|
+
Win32Console.enable_ansi!
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# @return [Boolean] Whether color output is suppressed (NO_COLOR, a non-TTY
|
|
158
|
+
# target, or TERM=dumb, unless overridden by FORCE_COLOR/force_terminal)
|
|
159
|
+
def no_color?
|
|
160
|
+
@no_color
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# @return [Boolean] Whether output is a terminal
|
|
164
|
+
def is_terminal?
|
|
165
|
+
return @force_terminal unless @force_terminal.nil?
|
|
166
|
+
@file.respond_to?(:tty?) && @file.tty?
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# @return [Boolean] Is output a terminal
|
|
170
|
+
def terminal?
|
|
171
|
+
return @force_terminal unless @force_terminal.nil?
|
|
172
|
+
return false unless @file.respond_to?(:tty?)
|
|
173
|
+
|
|
174
|
+
@file.tty?
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# @return [Boolean] Is this a legacy Windows console
|
|
178
|
+
def legacy_windows?
|
|
179
|
+
@legacy_windows
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# @return [Integer] Console width in characters
|
|
183
|
+
def width
|
|
184
|
+
return @width_override if @width_override
|
|
185
|
+
|
|
186
|
+
detect_size[0]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# @return [Integer] Console height in characters
|
|
190
|
+
def height
|
|
191
|
+
return @height_override if @height_override
|
|
192
|
+
|
|
193
|
+
detect_size[1]
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# @return [Array<Integer>] [width, height]
|
|
197
|
+
def size
|
|
198
|
+
[width, height]
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Get console options for rendering
|
|
202
|
+
# @return [ConsoleOptions]
|
|
203
|
+
def options
|
|
204
|
+
ConsoleOptions.new(
|
|
205
|
+
min_width: 1,
|
|
206
|
+
max_width: width,
|
|
207
|
+
height: height,
|
|
208
|
+
legacy_windows: @legacy_windows,
|
|
209
|
+
encoding: encoding,
|
|
210
|
+
is_terminal: terminal?,
|
|
211
|
+
highlight: @highlight,
|
|
212
|
+
markup: @markup
|
|
213
|
+
)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# @return [String] Output encoding
|
|
217
|
+
def encoding
|
|
218
|
+
@file.respond_to?(:encoding) ? @file.encoding.to_s : "utf-8"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Print objects to the console
|
|
222
|
+
# @param objects [Array] Objects to print
|
|
223
|
+
# @param sep [String] Separator
|
|
224
|
+
# @param end_str [String] End string
|
|
225
|
+
# @param style [String, Style, nil] Style
|
|
226
|
+
# @param highlight [Boolean] Enable highlighting
|
|
227
|
+
# @return [void]
|
|
228
|
+
def print(*objects, sep: " ", end_str: "\n", style: nil, highlight: nil)
|
|
229
|
+
highlight = @highlight if highlight.nil?
|
|
230
|
+
base_style = style.is_a?(String) ? Style.parse(style) : style
|
|
231
|
+
|
|
232
|
+
segments = []
|
|
233
|
+
objects.each_with_index do |obj, index|
|
|
234
|
+
segments.concat(render_object(obj, highlight: highlight))
|
|
235
|
+
segments << Segment.new(sep) if sep && !sep.empty? && index < objects.length - 1
|
|
236
|
+
end
|
|
237
|
+
segments << Segment.new(end_str) if end_str && !end_str.empty?
|
|
238
|
+
|
|
239
|
+
segments = Segment.apply_style(segments, style: base_style) if base_style
|
|
240
|
+
|
|
241
|
+
write_segments(segments)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Print with markup parsing
|
|
245
|
+
# @param text [String] Text with markup
|
|
246
|
+
# @return [void]
|
|
247
|
+
def print_markup(text)
|
|
248
|
+
write_segments(Markup.parse(text).to_segments)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Print JSON with highlighting
|
|
252
|
+
# @param json [String, nil] JSON string
|
|
253
|
+
# @param data [Object] Data to convert
|
|
254
|
+
# @param indent [Integer] Indentation
|
|
255
|
+
# @return [void]
|
|
256
|
+
def print_json(json = nil, data: nil, indent: 2)
|
|
257
|
+
require "json"
|
|
258
|
+
|
|
259
|
+
json_str = json || ::JSON.pretty_generate(data, indent: " " * indent)
|
|
260
|
+
|
|
261
|
+
if @highlight
|
|
262
|
+
# Colorize JSON
|
|
263
|
+
highlighted = colorize_json(json_str)
|
|
264
|
+
print(highlighted)
|
|
265
|
+
else
|
|
266
|
+
print(json_str)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Print a horizontal rule
|
|
271
|
+
# @param title [String] Title
|
|
272
|
+
# @param style [String] Style
|
|
273
|
+
# @return [void]
|
|
274
|
+
def rule(title = "", style: "rule.line")
|
|
275
|
+
console_width = width
|
|
276
|
+
rule_style = Style.parse(style)
|
|
277
|
+
|
|
278
|
+
if title.empty?
|
|
279
|
+
line = "─" * console_width
|
|
280
|
+
write_styled(line + "\n", rule_style)
|
|
281
|
+
else
|
|
282
|
+
title_length = Cells.cell_len(title) + 2
|
|
283
|
+
remaining = console_width - title_length
|
|
284
|
+
|
|
285
|
+
if remaining > 0
|
|
286
|
+
left_width = remaining / 2
|
|
287
|
+
right_width = remaining - left_width
|
|
288
|
+
line = "─" * left_width + " #{title} " + "─" * right_width
|
|
289
|
+
else
|
|
290
|
+
line = title
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
write_styled(line + "\n", rule_style)
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Clear the screen
|
|
298
|
+
# @return [void]
|
|
299
|
+
def clear
|
|
300
|
+
if @legacy_windows && defined?(Win32Console)
|
|
301
|
+
Win32Console.clear_screen
|
|
302
|
+
else
|
|
303
|
+
write(Control.clear_screen)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Show the cursor
|
|
308
|
+
# @return [void]
|
|
309
|
+
def show_cursor
|
|
310
|
+
if @legacy_windows && defined?(Win32Console)
|
|
311
|
+
Win32Console.show_cursor
|
|
312
|
+
else
|
|
313
|
+
write(Control.show_cursor)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Hide the cursor
|
|
318
|
+
# @return [void]
|
|
319
|
+
def hide_cursor
|
|
320
|
+
if @legacy_windows && defined?(Win32Console)
|
|
321
|
+
Win32Console.hide_cursor
|
|
322
|
+
else
|
|
323
|
+
write(Control.hide_cursor)
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Set window title
|
|
328
|
+
# @param title [String] Window title
|
|
329
|
+
# @return [void]
|
|
330
|
+
def set_title(title)
|
|
331
|
+
if @legacy_windows && defined?(Win32Console)
|
|
332
|
+
Win32Console.set_title(title)
|
|
333
|
+
else
|
|
334
|
+
write(Control.set_title(title))
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Write raw text
|
|
339
|
+
# @param text [String] Text to write
|
|
340
|
+
# @return [void]
|
|
341
|
+
def write(text)
|
|
342
|
+
@file.write(text)
|
|
343
|
+
@file.flush if @file.respond_to?(:flush)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Write styled text
|
|
347
|
+
# @param text [String] Text to write
|
|
348
|
+
# @param style [Style] Style to apply
|
|
349
|
+
# @return [void]
|
|
350
|
+
def write_styled(text, style)
|
|
351
|
+
if @no_color
|
|
352
|
+
write(text)
|
|
353
|
+
elsif @legacy_windows && defined?(Win32Console)
|
|
354
|
+
write_styled_legacy(text, style)
|
|
355
|
+
else
|
|
356
|
+
rendered = style.render(color_system: @color_system)
|
|
357
|
+
write("#{rendered}#{text}\e[0m")
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Write segments to output
|
|
362
|
+
# @param segments [Array<Segment>] Segments to write
|
|
363
|
+
# @return [void]
|
|
364
|
+
def write_segments(segments)
|
|
365
|
+
if @no_color
|
|
366
|
+
segments.each { |segment| write(segment.text) unless segment.control? }
|
|
367
|
+
elsif @legacy_windows && defined?(Win32Console)
|
|
368
|
+
# Legacy console cannot interpret ANSI; drive colors via the Console API.
|
|
369
|
+
segments.each do |segment|
|
|
370
|
+
if segment.control?
|
|
371
|
+
segment.control.each { |code| write(Control.generate(*code)) }
|
|
372
|
+
elsif segment.style
|
|
373
|
+
write_styled_legacy(segment.text, segment.style)
|
|
374
|
+
else
|
|
375
|
+
write(segment.text)
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
else
|
|
379
|
+
write(Segment.render(segments, color_system: @color_system))
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Inspect an object
|
|
384
|
+
# @param obj [Object] Object to inspect
|
|
385
|
+
# @param title [String, nil] Title
|
|
386
|
+
# @param methods [Boolean] Show methods
|
|
387
|
+
# @param docs [Boolean] Show docs
|
|
388
|
+
# @return [void]
|
|
389
|
+
def inspect(obj, title: nil, methods: false, docs: true)
|
|
390
|
+
title ||= obj.class.name
|
|
391
|
+
|
|
392
|
+
rule(title, style: "bold")
|
|
393
|
+
|
|
394
|
+
print("Class: #{obj.class}")
|
|
395
|
+
print("Object ID: #{obj.object_id}")
|
|
396
|
+
|
|
397
|
+
if obj.respond_to?(:instance_variables)
|
|
398
|
+
ivars = obj.instance_variables
|
|
399
|
+
unless ivars.empty?
|
|
400
|
+
print("\nInstance Variables:")
|
|
401
|
+
ivars.each do |ivar|
|
|
402
|
+
value = obj.instance_variable_get(ivar)
|
|
403
|
+
print(" #{ivar}: #{value.inspect}")
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
if methods && obj.respond_to?(:methods)
|
|
409
|
+
obj_methods = (obj.methods - Object.methods).sort
|
|
410
|
+
unless obj_methods.empty?
|
|
411
|
+
print("\nMethods:")
|
|
412
|
+
obj_methods.each do |method|
|
|
413
|
+
print(" #{method}")
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
rule(style: "bold")
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
private
|
|
422
|
+
|
|
423
|
+
# Decide whether to suppress color, honoring the cross-platform
|
|
424
|
+
# conventions (https://no-color.org and FORCE_COLOR) plus TTY detection.
|
|
425
|
+
# Precedence: explicit force_terminal:false > NO_COLOR/TERM=dumb >
|
|
426
|
+
# FORCE_COLOR/force_terminal:true > whether the output is a TTY.
|
|
427
|
+
def compute_no_color
|
|
428
|
+
return true if @force_terminal == false
|
|
429
|
+
return true if ENV.key?("NO_COLOR") && !ENV["NO_COLOR"].to_s.empty?
|
|
430
|
+
return true if (ENV["TERM"] || "").casecmp?("dumb")
|
|
431
|
+
|
|
432
|
+
return false if @force_terminal == true
|
|
433
|
+
return false if ENV["FORCE_COLOR"] && !ENV["FORCE_COLOR"].to_s.empty?
|
|
434
|
+
|
|
435
|
+
!terminal?
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def detect_color_system
|
|
439
|
+
return ColorSystem::WINDOWS if Gem.win_platform? && !ansi_supported?
|
|
440
|
+
|
|
441
|
+
# Check terminal capabilities
|
|
442
|
+
term = ENV["TERM"] || ""
|
|
443
|
+
colorterm = ENV["COLORTERM"] || ""
|
|
444
|
+
|
|
445
|
+
if colorterm.downcase.include?("truecolor") || colorterm.downcase.include?("24bit")
|
|
446
|
+
return ColorSystem::TRUECOLOR
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
if term.include?("256color") || ENV["TERM_PROGRAM"] == "iTerm.app"
|
|
450
|
+
return ColorSystem::EIGHT_BIT
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
if %w[xterm vt100 screen].any? { |t| term.include?(t) }
|
|
454
|
+
return ColorSystem::STANDARD
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# Default to truecolor for modern terminals
|
|
458
|
+
ColorSystem::TRUECOLOR
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def detect_legacy_windows
|
|
462
|
+
return false unless Gem.win_platform?
|
|
463
|
+
return false if ansi_supported?
|
|
464
|
+
|
|
465
|
+
true
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def ansi_supported?
|
|
469
|
+
return true unless Gem.win_platform?
|
|
470
|
+
return true unless defined?(Win32Console)
|
|
471
|
+
|
|
472
|
+
Win32Console.supports_ansi?
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def detect_size
|
|
476
|
+
# Try Ruby's built-in IO#winsize
|
|
477
|
+
if @file.respond_to?(:winsize)
|
|
478
|
+
begin
|
|
479
|
+
height, width = @file.winsize
|
|
480
|
+
return [width, height] if width > 0 && height > 0
|
|
481
|
+
rescue StandardError
|
|
482
|
+
# Fall through
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# Try Windows API
|
|
487
|
+
if Gem.win_platform? && defined?(Win32Console)
|
|
488
|
+
size = Win32Console.get_size
|
|
489
|
+
return size if size
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# Try environment variables (decoupled: COLUMNS and LINES may be set
|
|
493
|
+
# independently — a common situation on Unix).
|
|
494
|
+
cols = ENV["COLUMNS"]&.to_i
|
|
495
|
+
rows = ENV["LINES"]&.to_i
|
|
496
|
+
if (cols && cols > 0) || (rows && rows > 0)
|
|
497
|
+
return [cols && cols > 0 ? cols : 80, rows && rows > 0 ? rows : 24]
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# Unix fallback: ask the controlling terminal via stty.
|
|
501
|
+
unless Gem.win_platform?
|
|
502
|
+
size = detect_size_via_stty
|
|
503
|
+
return size if size
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# Default
|
|
507
|
+
[80, 24]
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
# Query terminal size with `stty size` against the controlling terminal.
|
|
511
|
+
# @return [Array<Integer>, nil] [width, height] or nil if unavailable
|
|
512
|
+
def detect_size_via_stty
|
|
513
|
+
out = `stty size < /dev/tty 2>/dev/null`
|
|
514
|
+
rows, cols = out.split.map(&:to_i)
|
|
515
|
+
return [cols, rows] if cols && cols > 0 && rows && rows > 0
|
|
516
|
+
|
|
517
|
+
nil
|
|
518
|
+
rescue StandardError
|
|
519
|
+
nil
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# Convert any printable object into an array of Segments.
|
|
523
|
+
# Strings honor markup (when enabled); Rich renderables are rendered via
|
|
524
|
+
# their to_segments/render protocol; everything else falls back to to_s.
|
|
525
|
+
# @return [Array<Segment>]
|
|
526
|
+
def render_object(obj, highlight: true)
|
|
527
|
+
case obj
|
|
528
|
+
when Segment
|
|
529
|
+
[obj]
|
|
530
|
+
when String
|
|
531
|
+
@markup ? Markup.parse(obj).to_segments : [Segment.new(obj)]
|
|
532
|
+
when Text
|
|
533
|
+
obj.to_segments
|
|
534
|
+
else
|
|
535
|
+
if obj.respond_to?(:to_segments)
|
|
536
|
+
if accepts_keyword?(obj, :to_segments, :max_width)
|
|
537
|
+
obj.to_segments(max_width: width)
|
|
538
|
+
else
|
|
539
|
+
obj.to_segments
|
|
540
|
+
end
|
|
541
|
+
elsif obj.respond_to?(:render) && obj.method(:render).owner != Object
|
|
542
|
+
[Segment.new(render_renderable(obj))]
|
|
543
|
+
else
|
|
544
|
+
[Segment.new(obj.to_s)]
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
# Call a renderable's #render passing only the keywords it accepts.
|
|
550
|
+
def render_renderable(obj)
|
|
551
|
+
kwargs = {}
|
|
552
|
+
kwargs[:max_width] = width if accepts_keyword?(obj, :render, :max_width)
|
|
553
|
+
kwargs[:color_system] = @color_system if accepts_keyword?(obj, :render, :color_system)
|
|
554
|
+
kwargs.empty? ? obj.render : obj.render(**kwargs)
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
# @return [Boolean] whether obj#method declares the given keyword parameter
|
|
558
|
+
def accepts_keyword?(obj, method_name, keyword)
|
|
559
|
+
obj.method(method_name).parameters.any? do |type, name|
|
|
560
|
+
(type == :key || type == :keyreq) && name == keyword
|
|
561
|
+
end
|
|
562
|
+
rescue NameError
|
|
563
|
+
false
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def colorize_json(json_str)
|
|
567
|
+
# Simple JSON colorization
|
|
568
|
+
json_str
|
|
569
|
+
.gsub(/"([^"]+)"(?=\s*:)/) { "[cyan]\"#{Regexp.last_match(1)}\"[/]" } # Keys
|
|
570
|
+
.gsub(/:\s*"([^"]*)"/) { ": [green]\"#{Regexp.last_match(1)}\"[/]" } # String values
|
|
571
|
+
.gsub(/:\s*(\d+\.?\d*)/) { ": [yellow]#{Regexp.last_match(1)}[/]" } # Numbers
|
|
572
|
+
.gsub(/:\s*(true|false)/) { ": [italic]#{Regexp.last_match(1)}[/]" } # Booleans
|
|
573
|
+
.gsub(/:\s*null/) { ": [dim]null[/]" } # Null
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def write_styled_legacy(text, style)
|
|
577
|
+
return write(text) unless defined?(Win32Console)
|
|
578
|
+
|
|
579
|
+
# Map style to Windows console attributes. Colors must be reduced to a
|
|
580
|
+
# 0-15 index first; an 8-bit/truecolor number (>= 16) would otherwise be
|
|
581
|
+
# dropped to attribute 0 (black on black) and render invisibly.
|
|
582
|
+
fg = legacy_color_index(style.color) || 7
|
|
583
|
+
bg = legacy_color_index(style.bgcolor) || 0
|
|
584
|
+
|
|
585
|
+
# Apply bold as bright
|
|
586
|
+
fg |= 8 if style.bold?
|
|
587
|
+
|
|
588
|
+
attributes = Win32Console.ansi_to_windows_attributes(foreground: fg, background: bg)
|
|
589
|
+
original_attrs = Win32Console.get_text_attributes
|
|
590
|
+
|
|
591
|
+
Win32Console.set_text_attribute(attributes)
|
|
592
|
+
write(text)
|
|
593
|
+
Win32Console.set_text_attribute(original_attrs) if original_attrs
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
# Reduce any color to a 0-15 Windows console index (nil if no color).
|
|
597
|
+
def legacy_color_index(color)
|
|
598
|
+
return nil unless color
|
|
599
|
+
|
|
600
|
+
number = color.downgrade(ColorSystem::WINDOWS).number
|
|
601
|
+
number && number < 16 ? number : nil
|
|
602
|
+
end
|
|
603
|
+
end
|
|
604
|
+
end
|