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/layout.rb
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "segment"
|
|
4
|
+
require_relative "cells"
|
|
5
|
+
require_relative "style"
|
|
6
|
+
|
|
7
|
+
module Rich
|
|
8
|
+
# Display content side by side in columns
|
|
9
|
+
class Columns
|
|
10
|
+
# @return [Array] Items to display
|
|
11
|
+
attr_reader :items
|
|
12
|
+
|
|
13
|
+
# @return [Integer, nil] Column count
|
|
14
|
+
attr_reader :column_count
|
|
15
|
+
|
|
16
|
+
# @return [Integer] Padding between columns
|
|
17
|
+
attr_reader :padding
|
|
18
|
+
|
|
19
|
+
# @return [Boolean] Equal width columns
|
|
20
|
+
attr_reader :equal
|
|
21
|
+
|
|
22
|
+
# @return [Boolean] Expand to fill width
|
|
23
|
+
attr_reader :expand
|
|
24
|
+
|
|
25
|
+
def initialize(
|
|
26
|
+
items = [],
|
|
27
|
+
column_count: nil,
|
|
28
|
+
padding: 1,
|
|
29
|
+
equal: false,
|
|
30
|
+
expand: true
|
|
31
|
+
)
|
|
32
|
+
@items = items.to_a
|
|
33
|
+
@column_count = column_count
|
|
34
|
+
@padding = padding
|
|
35
|
+
@equal = equal
|
|
36
|
+
@expand = expand
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Add an item
|
|
40
|
+
# @param item [Object] Item to add
|
|
41
|
+
# @return [self]
|
|
42
|
+
def add(item)
|
|
43
|
+
@items << item
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Render to segments
|
|
48
|
+
# @param max_width [Integer] Maximum width
|
|
49
|
+
# @return [Array<Segment>]
|
|
50
|
+
def to_segments(max_width: 80)
|
|
51
|
+
return [] if @items.empty?
|
|
52
|
+
|
|
53
|
+
segments = []
|
|
54
|
+
|
|
55
|
+
# Calculate column count if not specified
|
|
56
|
+
num_columns = @column_count || calculate_column_count(max_width)
|
|
57
|
+
num_columns = [num_columns, @items.length].min
|
|
58
|
+
|
|
59
|
+
# Calculate column widths
|
|
60
|
+
col_widths = calculate_widths(max_width, num_columns)
|
|
61
|
+
|
|
62
|
+
# Render items in rows
|
|
63
|
+
@items.each_slice(num_columns) do |row_items|
|
|
64
|
+
row_items.each_with_index do |item, col_index|
|
|
65
|
+
content = item.to_s
|
|
66
|
+
width = col_widths[col_index]
|
|
67
|
+
|
|
68
|
+
# Render content
|
|
69
|
+
content_width = Cells.cell_len(content)
|
|
70
|
+
if content_width > width
|
|
71
|
+
content = truncate(content, width)
|
|
72
|
+
content_width = Cells.cell_len(content)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
segments << Segment.new(content)
|
|
76
|
+
segments << Segment.new(" " * (width - content_width))
|
|
77
|
+
|
|
78
|
+
# Padding between columns (not after last)
|
|
79
|
+
if col_index < row_items.length - 1
|
|
80
|
+
segments << Segment.new(" " * @padding)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
segments << Segment.new("\n")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
segments
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Render to string
|
|
91
|
+
# @param max_width [Integer] Maximum width
|
|
92
|
+
# @param color_system [Symbol] Color system
|
|
93
|
+
# @return [String]
|
|
94
|
+
def render(max_width: 80, color_system: ColorSystem::TRUECOLOR)
|
|
95
|
+
Segment.render(to_segments(max_width: max_width), color_system: color_system)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def calculate_column_count(max_width)
|
|
101
|
+
return 1 if @items.empty?
|
|
102
|
+
|
|
103
|
+
# Calculate based on average item width
|
|
104
|
+
avg_width = @items.map { |i| Cells.cell_len(i.to_s) }.sum / @items.length
|
|
105
|
+
min_col_width = [avg_width, 10].max
|
|
106
|
+
|
|
107
|
+
((max_width + @padding) / (min_col_width + @padding)).clamp(1, 10)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def calculate_widths(max_width, num_columns)
|
|
111
|
+
available = max_width - (@padding * (num_columns - 1))
|
|
112
|
+
|
|
113
|
+
if @equal
|
|
114
|
+
width = available / num_columns
|
|
115
|
+
Array.new(num_columns, width)
|
|
116
|
+
else
|
|
117
|
+
# Calculate based on content
|
|
118
|
+
widths = Array.new(num_columns, 0)
|
|
119
|
+
|
|
120
|
+
@items.each_with_index do |item, index|
|
|
121
|
+
col = index % num_columns
|
|
122
|
+
item_width = Cells.cell_len(item.to_s)
|
|
123
|
+
widths[col] = [widths[col], item_width].max
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Scale if total exceeds available
|
|
127
|
+
total = widths.sum
|
|
128
|
+
if total > available
|
|
129
|
+
ratio = available.to_f / total
|
|
130
|
+
widths.map! { |w| [( w * ratio).to_i, 5].max }
|
|
131
|
+
elsif @expand
|
|
132
|
+
extra = (available - total) / num_columns
|
|
133
|
+
widths.map! { |w| w + extra }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
widths
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def truncate(text, max_width)
|
|
141
|
+
result = +""
|
|
142
|
+
current = 0
|
|
143
|
+
|
|
144
|
+
text.each_char do |char|
|
|
145
|
+
char_width = Cells.char_width(char)
|
|
146
|
+
break if current + char_width > max_width
|
|
147
|
+
|
|
148
|
+
result << char
|
|
149
|
+
current += char_width
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
result
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Live updating display
|
|
157
|
+
class Live
|
|
158
|
+
# @return [Console] Console for output
|
|
159
|
+
attr_reader :console
|
|
160
|
+
|
|
161
|
+
# @return [Float] Refresh rate
|
|
162
|
+
attr_reader :refresh_rate
|
|
163
|
+
|
|
164
|
+
# @return [Boolean] Transient (clear on exit)
|
|
165
|
+
attr_reader :transient
|
|
166
|
+
|
|
167
|
+
# Default refresh rate (Windows is slower)
|
|
168
|
+
DEFAULT_REFRESH = Gem.win_platform? ? 0.2 : 0.1
|
|
169
|
+
|
|
170
|
+
def initialize(
|
|
171
|
+
console: nil,
|
|
172
|
+
refresh_rate: DEFAULT_REFRESH,
|
|
173
|
+
transient: false
|
|
174
|
+
)
|
|
175
|
+
@console = console || Console.new
|
|
176
|
+
@refresh_rate = refresh_rate
|
|
177
|
+
@transient = transient
|
|
178
|
+
@renderable = nil
|
|
179
|
+
@started = false
|
|
180
|
+
@lines_rendered = 0
|
|
181
|
+
@last_render = nil
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Update the renderable content
|
|
185
|
+
# @param renderable [Object] Content to display
|
|
186
|
+
def update(renderable)
|
|
187
|
+
@renderable = renderable
|
|
188
|
+
refresh
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Start live display
|
|
192
|
+
# @yield Block to execute with live updates
|
|
193
|
+
def start
|
|
194
|
+
@started = true
|
|
195
|
+
@console.hide_cursor
|
|
196
|
+
|
|
197
|
+
if block_given?
|
|
198
|
+
begin
|
|
199
|
+
yield self
|
|
200
|
+
ensure
|
|
201
|
+
stop
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Stop live display
|
|
207
|
+
def stop
|
|
208
|
+
return unless @started
|
|
209
|
+
|
|
210
|
+
if @transient && @lines_rendered > 0
|
|
211
|
+
# Clear rendered content
|
|
212
|
+
@console.write("\e[#{@lines_rendered}A\e[J")
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
@console.show_cursor
|
|
216
|
+
@started = false
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Refresh display if needed
|
|
220
|
+
def refresh
|
|
221
|
+
return unless @started
|
|
222
|
+
return unless @renderable
|
|
223
|
+
|
|
224
|
+
now = Time.now
|
|
225
|
+
return if @last_render && now - @last_render < @refresh_rate
|
|
226
|
+
|
|
227
|
+
render_update
|
|
228
|
+
@last_render = now
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
private
|
|
232
|
+
|
|
233
|
+
def render_update
|
|
234
|
+
# Clear previous output
|
|
235
|
+
if @lines_rendered > 0
|
|
236
|
+
@console.write("\e[#{@lines_rendered}A\e[J")
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Render new content
|
|
240
|
+
output = render_content(@renderable)
|
|
241
|
+
@console.write(output)
|
|
242
|
+
|
|
243
|
+
# Count lines
|
|
244
|
+
@lines_rendered = output.count("\n")
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def render_content(obj)
|
|
248
|
+
case obj
|
|
249
|
+
when Panel, Table, Tree
|
|
250
|
+
obj.render(max_width: @console.width, color_system: @console.color_system)
|
|
251
|
+
else
|
|
252
|
+
"#{obj}\n"
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Status display with spinner
|
|
258
|
+
class Status
|
|
259
|
+
# @return [Spinner] Spinner animation
|
|
260
|
+
attr_reader :spinner
|
|
261
|
+
|
|
262
|
+
# @return [String] Status message
|
|
263
|
+
attr_accessor :message
|
|
264
|
+
|
|
265
|
+
# @return [Console] Console
|
|
266
|
+
attr_reader :console
|
|
267
|
+
|
|
268
|
+
def initialize(message = "", console: nil, spinner: nil)
|
|
269
|
+
@message = message
|
|
270
|
+
@console = console || Console.new
|
|
271
|
+
@spinner = spinner || Spinner.new
|
|
272
|
+
@started = false
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Start status display
|
|
276
|
+
# @yield Block to execute
|
|
277
|
+
def start
|
|
278
|
+
@started = true
|
|
279
|
+
@console.hide_cursor
|
|
280
|
+
|
|
281
|
+
if block_given?
|
|
282
|
+
begin
|
|
283
|
+
yield self
|
|
284
|
+
ensure
|
|
285
|
+
stop
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Stop status display
|
|
291
|
+
def stop
|
|
292
|
+
return unless @started
|
|
293
|
+
|
|
294
|
+
@console.write("\r\e[K")
|
|
295
|
+
@console.show_cursor
|
|
296
|
+
@started = false
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Update message
|
|
300
|
+
# @param new_message [String] New message
|
|
301
|
+
def update(new_message)
|
|
302
|
+
@message = new_message
|
|
303
|
+
refresh
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Refresh display
|
|
307
|
+
def refresh
|
|
308
|
+
return unless @started
|
|
309
|
+
|
|
310
|
+
@spinner.update
|
|
311
|
+
@console.write("\r\e[K#{@spinner.frame} #{@message}")
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|