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
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "style"
|
|
4
|
+
require_relative "segment"
|
|
5
|
+
require_relative "cells"
|
|
6
|
+
|
|
7
|
+
module Rich
|
|
8
|
+
# Progress bar styles
|
|
9
|
+
module ProgressStyle
|
|
10
|
+
# Bar characters
|
|
11
|
+
BAR_FILLED = "━"
|
|
12
|
+
BAR_UNFILLED = "━"
|
|
13
|
+
BAR_START = ""
|
|
14
|
+
BAR_END = ""
|
|
15
|
+
|
|
16
|
+
# Spinner frames
|
|
17
|
+
DOTS = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏].freeze
|
|
18
|
+
LINE = %w[| / - \\].freeze
|
|
19
|
+
ARROW = %w[←↖↑↗→↘↓↙].freeze
|
|
20
|
+
CIRCLE = %w[◐ ◓ ◑ ◒].freeze
|
|
21
|
+
MOON = %w[🌑 🌒 🌓 🌔 🌕 🌖 🌗 🌘].freeze
|
|
22
|
+
BOUNCE = %w[⠁ ⠂ ⠄ ⡀ ⢀ ⠠ ⠐ ⠈].freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# A spinning animation indicator
|
|
26
|
+
class Spinner
|
|
27
|
+
# @return [Array<String>] Spinner frames
|
|
28
|
+
attr_reader :frames
|
|
29
|
+
|
|
30
|
+
# @return [Style, nil] Style
|
|
31
|
+
attr_reader :style
|
|
32
|
+
|
|
33
|
+
# @return [Float] Speed (seconds per frame)
|
|
34
|
+
attr_reader :speed
|
|
35
|
+
|
|
36
|
+
def initialize(frames: ProgressStyle::DOTS, style: nil, speed: 0.1)
|
|
37
|
+
@frames = frames
|
|
38
|
+
@style = style.is_a?(String) ? Style.parse(style) : style
|
|
39
|
+
@speed = speed
|
|
40
|
+
@frame_index = 0
|
|
41
|
+
@last_update = Time.now
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Get current frame
|
|
45
|
+
# @return [String]
|
|
46
|
+
def frame
|
|
47
|
+
@frames[@frame_index % @frames.length]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Advance to next frame
|
|
51
|
+
# @return [String] Current frame after advance
|
|
52
|
+
def advance
|
|
53
|
+
@frame_index = (@frame_index + 1) % @frames.length
|
|
54
|
+
@last_update = Time.now
|
|
55
|
+
frame
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Update if enough time has passed
|
|
59
|
+
# @return [Boolean] True if frame changed
|
|
60
|
+
def update
|
|
61
|
+
if Time.now - @last_update >= @speed
|
|
62
|
+
advance
|
|
63
|
+
true
|
|
64
|
+
else
|
|
65
|
+
false
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Get segment for current frame
|
|
70
|
+
# @return [Segment]
|
|
71
|
+
def to_segment
|
|
72
|
+
Segment.new(frame, style: @style)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Reset to first frame
|
|
76
|
+
def reset
|
|
77
|
+
@frame_index = 0
|
|
78
|
+
@last_update = Time.now
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# A progress bar for tracking task completion
|
|
83
|
+
class ProgressBar
|
|
84
|
+
# @return [Integer] Total steps
|
|
85
|
+
attr_reader :total
|
|
86
|
+
|
|
87
|
+
# @return [Integer] Completed steps
|
|
88
|
+
attr_reader :completed
|
|
89
|
+
|
|
90
|
+
# @return [Integer] Width of the bar
|
|
91
|
+
attr_reader :width
|
|
92
|
+
|
|
93
|
+
# @return [Style, nil] Completed portion style
|
|
94
|
+
attr_reader :complete_style
|
|
95
|
+
|
|
96
|
+
# @return [Style, nil] Remaining portion style
|
|
97
|
+
attr_reader :incomplete_style
|
|
98
|
+
|
|
99
|
+
# @return [Style, nil] Finished style
|
|
100
|
+
attr_reader :finished_style
|
|
101
|
+
|
|
102
|
+
# @return [Boolean] Show percentage
|
|
103
|
+
attr_reader :show_percentage
|
|
104
|
+
|
|
105
|
+
# @return [Boolean] Pulse animation
|
|
106
|
+
attr_reader :pulse
|
|
107
|
+
|
|
108
|
+
# @return [String] Bar character (filled)
|
|
109
|
+
attr_reader :bar_char
|
|
110
|
+
|
|
111
|
+
# @return [String] Bar character (unfilled)
|
|
112
|
+
attr_reader :unfilled_char
|
|
113
|
+
|
|
114
|
+
def initialize(
|
|
115
|
+
total: 100,
|
|
116
|
+
completed: 0,
|
|
117
|
+
width: 40,
|
|
118
|
+
complete_style: "bar.complete",
|
|
119
|
+
incomplete_style: "bar.back",
|
|
120
|
+
finished_style: "bar.finished",
|
|
121
|
+
show_percentage: true,
|
|
122
|
+
pulse: false,
|
|
123
|
+
bar_char: "━",
|
|
124
|
+
unfilled_char: "━"
|
|
125
|
+
)
|
|
126
|
+
@total = [total, 1].max
|
|
127
|
+
@completed = [completed, 0].max
|
|
128
|
+
@width = width
|
|
129
|
+
@complete_style = complete_style.is_a?(String) ? Style.parse(complete_style) : complete_style
|
|
130
|
+
@incomplete_style = incomplete_style.is_a?(String) ? Style.parse(incomplete_style) : incomplete_style
|
|
131
|
+
@finished_style = finished_style.is_a?(String) ? Style.parse(finished_style) : finished_style
|
|
132
|
+
@show_percentage = show_percentage
|
|
133
|
+
@pulse = pulse
|
|
134
|
+
@bar_char = bar_char
|
|
135
|
+
@unfilled_char = unfilled_char
|
|
136
|
+
@start_time = nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# @return [Float] Progress as fraction (0.0 to 1.0)
|
|
140
|
+
def progress
|
|
141
|
+
@completed.to_f / @total
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# @return [Integer] Progress as percentage (0 to 100)
|
|
145
|
+
def percentage
|
|
146
|
+
(progress * 100).round
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# @return [Boolean] True if complete
|
|
150
|
+
def finished?
|
|
151
|
+
@completed >= @total
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Update progress
|
|
155
|
+
# @param advance [Integer] Steps to advance
|
|
156
|
+
# @return [self]
|
|
157
|
+
def advance(steps = 1)
|
|
158
|
+
@start_time ||= Time.now
|
|
159
|
+
@completed = [@completed + steps, @total].min
|
|
160
|
+
self
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Set completed value directly
|
|
164
|
+
# @param value [Integer] Completed steps
|
|
165
|
+
# @return [self]
|
|
166
|
+
def update(value)
|
|
167
|
+
@start_time ||= Time.now
|
|
168
|
+
@completed = [[value, 0].max, @total].min
|
|
169
|
+
self
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Reset progress
|
|
173
|
+
# @return [self]
|
|
174
|
+
def reset
|
|
175
|
+
@completed = 0
|
|
176
|
+
@start_time = nil
|
|
177
|
+
self
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Elapsed time since start
|
|
181
|
+
# @return [Float, nil] Seconds elapsed
|
|
182
|
+
def elapsed
|
|
183
|
+
return nil unless @start_time
|
|
184
|
+
|
|
185
|
+
Time.now - @start_time
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Estimated time remaining
|
|
189
|
+
# @return [Float, nil] Seconds remaining
|
|
190
|
+
def eta
|
|
191
|
+
return nil unless @start_time && progress > 0
|
|
192
|
+
|
|
193
|
+
elapsed_time = elapsed
|
|
194
|
+
total_estimated = elapsed_time / progress
|
|
195
|
+
total_estimated - elapsed_time
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Format time as string
|
|
199
|
+
# @param seconds [Float] Seconds
|
|
200
|
+
# @return [String] Formatted time
|
|
201
|
+
def format_time(seconds)
|
|
202
|
+
return "--:--" unless seconds
|
|
203
|
+
|
|
204
|
+
mins = (seconds / 60).floor
|
|
205
|
+
secs = (seconds % 60).floor
|
|
206
|
+
|
|
207
|
+
if mins >= 60
|
|
208
|
+
hours = (mins / 60).floor
|
|
209
|
+
mins = mins % 60
|
|
210
|
+
format("%d:%02d:%02d", hours, mins, secs)
|
|
211
|
+
else
|
|
212
|
+
format("%d:%02d", mins, secs)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Render progress bar to segments
|
|
217
|
+
# @return [Array<Segment>]
|
|
218
|
+
def to_segments
|
|
219
|
+
segments = []
|
|
220
|
+
|
|
221
|
+
filled_width = (progress * @width).round
|
|
222
|
+
unfilled_width = @width - filled_width
|
|
223
|
+
|
|
224
|
+
# Bar
|
|
225
|
+
style = finished? ? @finished_style : @complete_style
|
|
226
|
+
|
|
227
|
+
if filled_width > 0
|
|
228
|
+
segments << Segment.new(@bar_char * filled_width, style: style)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
if unfilled_width > 0
|
|
232
|
+
segments << Segment.new(@unfilled_char * unfilled_width, style: @incomplete_style)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Percentage
|
|
236
|
+
if @show_percentage
|
|
237
|
+
segments << Segment.new(" #{percentage}%")
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
segments
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Render to string with ANSI codes
|
|
244
|
+
# @param color_system [Symbol] Color system
|
|
245
|
+
# @return [String]
|
|
246
|
+
def render(color_system: ColorSystem::TRUECOLOR)
|
|
247
|
+
Segment.render(to_segments, color_system: color_system)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# A task in progress tracking
|
|
252
|
+
class ProgressTask
|
|
253
|
+
# @return [String] Task description
|
|
254
|
+
attr_reader :description
|
|
255
|
+
|
|
256
|
+
# @return [Integer] Total steps
|
|
257
|
+
attr_reader :total
|
|
258
|
+
|
|
259
|
+
# @return [Integer] Completed steps
|
|
260
|
+
attr_reader :completed
|
|
261
|
+
|
|
262
|
+
# @return [Boolean] Task is finished
|
|
263
|
+
attr_reader :finished
|
|
264
|
+
|
|
265
|
+
# @return [Time] Start time
|
|
266
|
+
attr_reader :start_time
|
|
267
|
+
|
|
268
|
+
# @return [Time, nil] End time
|
|
269
|
+
attr_reader :end_time
|
|
270
|
+
|
|
271
|
+
def initialize(description:, total: 100)
|
|
272
|
+
@description = description
|
|
273
|
+
@total = total
|
|
274
|
+
@completed = 0
|
|
275
|
+
@finished = false
|
|
276
|
+
@start_time = Time.now
|
|
277
|
+
@end_time = nil
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Update progress
|
|
281
|
+
# @param advance [Integer] Steps to advance
|
|
282
|
+
def advance(steps = 1)
|
|
283
|
+
@completed = [@completed + steps, @total].min
|
|
284
|
+
finish if @completed >= @total
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Set completed directly
|
|
288
|
+
# @param value [Integer] Completed value
|
|
289
|
+
def update(value)
|
|
290
|
+
@completed = [[value, 0].max, @total].min
|
|
291
|
+
finish if @completed >= @total
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Mark as finished
|
|
295
|
+
def finish
|
|
296
|
+
@finished = true
|
|
297
|
+
@end_time = Time.now
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# @return [Float] Progress fraction
|
|
301
|
+
def progress
|
|
302
|
+
@completed.to_f / @total
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# @return [Float, nil] Elapsed time
|
|
306
|
+
def elapsed
|
|
307
|
+
(@end_time || Time.now) - @start_time
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Progress display for multiple tasks
|
|
312
|
+
class Progress
|
|
313
|
+
# Windows has slower console, so refresh less frequently
|
|
314
|
+
DEFAULT_REFRESH = Gem.win_platform? ? 0.2 : 0.1
|
|
315
|
+
|
|
316
|
+
# @return [Array<ProgressTask>] Tasks
|
|
317
|
+
attr_reader :tasks
|
|
318
|
+
|
|
319
|
+
# @return [Console] Console for output
|
|
320
|
+
attr_reader :console
|
|
321
|
+
|
|
322
|
+
# @return [Float] Refresh interval
|
|
323
|
+
attr_reader :refresh_rate
|
|
324
|
+
|
|
325
|
+
def initialize(console: nil, refresh_rate: DEFAULT_REFRESH, transient: true)
|
|
326
|
+
@console = console || Console.new
|
|
327
|
+
@refresh_rate = refresh_rate
|
|
328
|
+
@transient = transient
|
|
329
|
+
@tasks = []
|
|
330
|
+
@started = false
|
|
331
|
+
@finished = false
|
|
332
|
+
@last_render = nil
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Add a new task
|
|
336
|
+
# @param description [String] Task description
|
|
337
|
+
# @param total [Integer] Total steps
|
|
338
|
+
# @return [ProgressTask]
|
|
339
|
+
def add_task(description, total: 100)
|
|
340
|
+
task = ProgressTask.new(description: description, total: total)
|
|
341
|
+
@tasks << task
|
|
342
|
+
task
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Start progress display
|
|
346
|
+
# @yield Block to execute with progress tracking
|
|
347
|
+
# @return [void]
|
|
348
|
+
def start
|
|
349
|
+
@started = true
|
|
350
|
+
@console.hide_cursor
|
|
351
|
+
|
|
352
|
+
if block_given?
|
|
353
|
+
begin
|
|
354
|
+
yield self
|
|
355
|
+
ensure
|
|
356
|
+
stop
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Stop progress display
|
|
362
|
+
# @return [void]
|
|
363
|
+
def stop
|
|
364
|
+
return unless @started
|
|
365
|
+
|
|
366
|
+
@finished = true
|
|
367
|
+
render_final
|
|
368
|
+
@console.show_cursor
|
|
369
|
+
@started = false
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# Refresh display if needed
|
|
373
|
+
# @return [void]
|
|
374
|
+
def refresh
|
|
375
|
+
return unless @started
|
|
376
|
+
|
|
377
|
+
now = Time.now
|
|
378
|
+
if @last_render.nil? || now - @last_render >= @refresh_rate
|
|
379
|
+
render
|
|
380
|
+
@last_render = now
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
private
|
|
385
|
+
|
|
386
|
+
def render
|
|
387
|
+
# Clear previous output
|
|
388
|
+
lines = @tasks.length
|
|
389
|
+
@console.write("\e[#{lines}A\e[J") if @last_render
|
|
390
|
+
|
|
391
|
+
@tasks.each do |task|
|
|
392
|
+
render_task(task)
|
|
393
|
+
@console.write("\n")
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def render_final
|
|
398
|
+
return if @transient
|
|
399
|
+
|
|
400
|
+
@tasks.each do |task|
|
|
401
|
+
render_task(task)
|
|
402
|
+
@console.write("\n")
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def render_task(task)
|
|
407
|
+
bar = ProgressBar.new(
|
|
408
|
+
total: task.total,
|
|
409
|
+
completed: task.completed,
|
|
410
|
+
width: 30,
|
|
411
|
+
complete_style: "green",
|
|
412
|
+
incomplete_style: "dim"
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
description = task.description.ljust(30)[0, 30]
|
|
416
|
+
percentage = "#{(task.progress * 100).round}%".rjust(4)
|
|
417
|
+
elapsed = format_time(task.elapsed)
|
|
418
|
+
|
|
419
|
+
@console.write("#{description} ")
|
|
420
|
+
@console.write_segments(bar.to_segments)
|
|
421
|
+
@console.write(" #{percentage} • #{elapsed}")
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def format_time(seconds)
|
|
425
|
+
mins = (seconds / 60).floor
|
|
426
|
+
secs = (seconds % 60).floor
|
|
427
|
+
format("%d:%02d", mins, secs)
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
end
|