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,537 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module AsciinemaWin
|
|
7
|
+
# Asciicast v2 file format handling
|
|
8
|
+
#
|
|
9
|
+
# This module implements the asciicast v2 specification for terminal recordings.
|
|
10
|
+
# Format: newline-delimited JSON (NDJSON)
|
|
11
|
+
# - Line 1: Header object with metadata
|
|
12
|
+
# - Lines 2+: Event arrays [time, type, data]
|
|
13
|
+
#
|
|
14
|
+
# @see https://docs.asciinema.org/manual/asciicast/v2/
|
|
15
|
+
module Asciicast
|
|
16
|
+
# Asciicast format version
|
|
17
|
+
VERSION = 2
|
|
18
|
+
|
|
19
|
+
# File extension
|
|
20
|
+
EXTENSION = ".cast"
|
|
21
|
+
|
|
22
|
+
# MIME type
|
|
23
|
+
MIME_TYPE = "application/x-asciicast"
|
|
24
|
+
|
|
25
|
+
# Event type constants
|
|
26
|
+
module EventType
|
|
27
|
+
# Output data written to terminal
|
|
28
|
+
OUTPUT = "o"
|
|
29
|
+
# Input data read from terminal
|
|
30
|
+
INPUT = "i"
|
|
31
|
+
# Terminal resize event
|
|
32
|
+
RESIZE = "r"
|
|
33
|
+
# Marker/bookmark
|
|
34
|
+
MARKER = "m"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Recording header with metadata
|
|
38
|
+
#
|
|
39
|
+
# @example Create a header
|
|
40
|
+
# header = Header.new(
|
|
41
|
+
# width: 120,
|
|
42
|
+
# height: 30,
|
|
43
|
+
# title: "Demo Recording"
|
|
44
|
+
# )
|
|
45
|
+
class Header
|
|
46
|
+
# @return [Integer] Format version (always 2)
|
|
47
|
+
attr_accessor :version
|
|
48
|
+
|
|
49
|
+
# @return [Integer] Terminal width in columns
|
|
50
|
+
attr_accessor :width
|
|
51
|
+
|
|
52
|
+
# @return [Integer] Terminal height in rows
|
|
53
|
+
attr_accessor :height
|
|
54
|
+
|
|
55
|
+
# @return [Integer, nil] Unix timestamp of recording start
|
|
56
|
+
attr_accessor :timestamp
|
|
57
|
+
|
|
58
|
+
# @return [Float, nil] Total duration in seconds
|
|
59
|
+
attr_accessor :duration
|
|
60
|
+
|
|
61
|
+
# @return [Float, nil] Maximum idle time between frames
|
|
62
|
+
attr_accessor :idle_time_limit
|
|
63
|
+
|
|
64
|
+
# @return [String, nil] Command that was recorded
|
|
65
|
+
attr_accessor :command
|
|
66
|
+
|
|
67
|
+
# @return [String, nil] Recording title
|
|
68
|
+
attr_accessor :title
|
|
69
|
+
|
|
70
|
+
# @return [Hash<String, String>] Captured environment variables
|
|
71
|
+
attr_accessor :env
|
|
72
|
+
|
|
73
|
+
# @return [Hash, nil] Terminal color theme
|
|
74
|
+
attr_accessor :theme
|
|
75
|
+
|
|
76
|
+
# Create a new header
|
|
77
|
+
#
|
|
78
|
+
# @param width [Integer] Terminal width
|
|
79
|
+
# @param height [Integer] Terminal height
|
|
80
|
+
# @param timestamp [Integer, nil] Unix timestamp (defaults to now)
|
|
81
|
+
# @param duration [Float, nil] Recording duration
|
|
82
|
+
# @param idle_time_limit [Float, nil] Max idle time
|
|
83
|
+
# @param command [String, nil] Recorded command
|
|
84
|
+
# @param title [String, nil] Recording title
|
|
85
|
+
# @param env [Hash, nil] Environment variables
|
|
86
|
+
# @param theme [Hash, nil] Color theme
|
|
87
|
+
def initialize(
|
|
88
|
+
width:,
|
|
89
|
+
height:,
|
|
90
|
+
timestamp: nil,
|
|
91
|
+
duration: nil,
|
|
92
|
+
idle_time_limit: nil,
|
|
93
|
+
command: nil,
|
|
94
|
+
title: nil,
|
|
95
|
+
env: nil,
|
|
96
|
+
theme: nil
|
|
97
|
+
)
|
|
98
|
+
@version = VERSION
|
|
99
|
+
@width = width
|
|
100
|
+
@height = height
|
|
101
|
+
@timestamp = timestamp || Time.now.to_i
|
|
102
|
+
@duration = duration
|
|
103
|
+
@idle_time_limit = idle_time_limit
|
|
104
|
+
@command = command
|
|
105
|
+
@title = title
|
|
106
|
+
@env = env || {}
|
|
107
|
+
@theme = theme
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Serialize header to JSON string
|
|
111
|
+
#
|
|
112
|
+
# @return [String] JSON representation
|
|
113
|
+
def to_json(*_args)
|
|
114
|
+
data = {
|
|
115
|
+
"version" => @version,
|
|
116
|
+
"width" => @width,
|
|
117
|
+
"height" => @height
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# Add optional fields only if present
|
|
121
|
+
data["timestamp"] = @timestamp if @timestamp
|
|
122
|
+
data["duration"] = @duration if @duration
|
|
123
|
+
data["idle_time_limit"] = @idle_time_limit if @idle_time_limit
|
|
124
|
+
data["command"] = @command if @command
|
|
125
|
+
data["title"] = @title if @title
|
|
126
|
+
data["env"] = @env unless @env.empty?
|
|
127
|
+
data["theme"] = @theme if @theme
|
|
128
|
+
|
|
129
|
+
JSON.generate(data)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Parse header from JSON string
|
|
133
|
+
#
|
|
134
|
+
# @param json_str [String] JSON string
|
|
135
|
+
# @return [Header] Parsed header
|
|
136
|
+
# @raise [FormatError] If JSON is invalid or required fields are missing
|
|
137
|
+
def self.from_json(json_str)
|
|
138
|
+
data = JSON.parse(json_str)
|
|
139
|
+
|
|
140
|
+
unless data.is_a?(Hash)
|
|
141
|
+
raise FormatError, "Header must be a JSON object"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
version = data["version"]
|
|
145
|
+
unless version == VERSION
|
|
146
|
+
raise FormatError, "Unsupported asciicast version: #{version}. Expected: #{VERSION}"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
width = data["width"]
|
|
150
|
+
height = data["height"]
|
|
151
|
+
|
|
152
|
+
unless width.is_a?(Integer) && width > 0
|
|
153
|
+
raise FormatError, "Invalid width: #{width}"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
unless height.is_a?(Integer) && height > 0
|
|
157
|
+
raise FormatError, "Invalid height: #{height}"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
new(
|
|
161
|
+
width: width,
|
|
162
|
+
height: height,
|
|
163
|
+
timestamp: data["timestamp"],
|
|
164
|
+
duration: data["duration"]&.to_f,
|
|
165
|
+
idle_time_limit: data["idle_time_limit"]&.to_f,
|
|
166
|
+
command: data["command"],
|
|
167
|
+
title: data["title"],
|
|
168
|
+
env: data["env"] || {},
|
|
169
|
+
theme: data["theme"]
|
|
170
|
+
)
|
|
171
|
+
rescue JSON::ParserError => e
|
|
172
|
+
raise FormatError, "Invalid header JSON: #{e.message}"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# @return [Hash] Header as a hash
|
|
176
|
+
def to_h
|
|
177
|
+
{
|
|
178
|
+
version: @version,
|
|
179
|
+
width: @width,
|
|
180
|
+
height: @height,
|
|
181
|
+
timestamp: @timestamp,
|
|
182
|
+
duration: @duration,
|
|
183
|
+
idle_time_limit: @idle_time_limit,
|
|
184
|
+
command: @command,
|
|
185
|
+
title: @title,
|
|
186
|
+
env: @env,
|
|
187
|
+
theme: @theme
|
|
188
|
+
}
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Single recording event
|
|
193
|
+
#
|
|
194
|
+
# @example Output event
|
|
195
|
+
# event = Event.new(0.5, EventType::OUTPUT, "Hello, World!")
|
|
196
|
+
#
|
|
197
|
+
# @example Resize event
|
|
198
|
+
# event = Event.new(1.0, EventType::RESIZE, "120x40")
|
|
199
|
+
class Event
|
|
200
|
+
# @return [Float] Time offset in seconds from recording start
|
|
201
|
+
attr_reader :time
|
|
202
|
+
|
|
203
|
+
# @return [String] Event type (o, i, r, m)
|
|
204
|
+
attr_reader :type
|
|
205
|
+
|
|
206
|
+
# @return [String] Event data
|
|
207
|
+
attr_reader :data
|
|
208
|
+
|
|
209
|
+
# Create a new event
|
|
210
|
+
#
|
|
211
|
+
# @param time [Float] Time offset in seconds
|
|
212
|
+
# @param type [String] Event type
|
|
213
|
+
# @param data [String] Event data
|
|
214
|
+
def initialize(time, type, data)
|
|
215
|
+
@time = time.to_f
|
|
216
|
+
@type = type.to_s
|
|
217
|
+
@data = data.to_s
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Serialize event to JSON array string
|
|
221
|
+
#
|
|
222
|
+
# @return [String] JSON array representation
|
|
223
|
+
def to_json(*_args)
|
|
224
|
+
JSON.generate([@time, @type, @data])
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Parse event from JSON string
|
|
228
|
+
#
|
|
229
|
+
# @param json_str [String] JSON array string
|
|
230
|
+
# @return [Event] Parsed event
|
|
231
|
+
# @raise [FormatError] If format is invalid
|
|
232
|
+
def self.from_json(json_str)
|
|
233
|
+
data = JSON.parse(json_str)
|
|
234
|
+
|
|
235
|
+
unless data.is_a?(Array) && data.length >= 3
|
|
236
|
+
raise FormatError, "Event must be a JSON array with 3 elements"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
new(data[0], data[1], data[2])
|
|
240
|
+
rescue JSON::ParserError => e
|
|
241
|
+
raise FormatError, "Invalid event JSON: #{e.message}"
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Check if this is an output event
|
|
245
|
+
# @return [Boolean]
|
|
246
|
+
def output?
|
|
247
|
+
@type == EventType::OUTPUT
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Check if this is an input event
|
|
251
|
+
# @return [Boolean]
|
|
252
|
+
def input?
|
|
253
|
+
@type == EventType::INPUT
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Check if this is a resize event
|
|
257
|
+
# @return [Boolean]
|
|
258
|
+
def resize?
|
|
259
|
+
@type == EventType::RESIZE
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Check if this is a marker event
|
|
263
|
+
# @return [Boolean]
|
|
264
|
+
def marker?
|
|
265
|
+
@type == EventType::MARKER
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Get resize dimensions (for resize events)
|
|
269
|
+
# @return [Array<Integer>, nil] [width, height] or nil if not a resize event
|
|
270
|
+
def resize_dimensions
|
|
271
|
+
return nil unless resize?
|
|
272
|
+
|
|
273
|
+
parts = @data.split("x")
|
|
274
|
+
return nil unless parts.length == 2
|
|
275
|
+
|
|
276
|
+
[parts[0].to_i, parts[1].to_i]
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# @return [Hash] Event as a hash
|
|
280
|
+
def to_h
|
|
281
|
+
{ time: @time, type: @type, data: @data }
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Writer for creating asciicast recordings
|
|
286
|
+
#
|
|
287
|
+
# @example Write a recording
|
|
288
|
+
# header = Asciicast::Header.new(width: 80, height: 24)
|
|
289
|
+
# File.open("recording.cast", "w") do |file|
|
|
290
|
+
# writer = Asciicast::Writer.new(file, header)
|
|
291
|
+
# writer.write_output(0.0, "Hello\r\n")
|
|
292
|
+
# writer.write_output(0.5, "World\r\n")
|
|
293
|
+
# writer.close
|
|
294
|
+
# end
|
|
295
|
+
class Writer
|
|
296
|
+
# @return [Header] Recording header
|
|
297
|
+
attr_reader :header
|
|
298
|
+
|
|
299
|
+
# @return [Float] Last event time (for duration calculation)
|
|
300
|
+
attr_reader :last_event_time
|
|
301
|
+
|
|
302
|
+
# Create a new writer
|
|
303
|
+
#
|
|
304
|
+
# @param io [IO] Output stream
|
|
305
|
+
# @param header [Header] Recording header
|
|
306
|
+
def initialize(io, header)
|
|
307
|
+
@io = io
|
|
308
|
+
@header = header
|
|
309
|
+
@last_event_time = 0.0
|
|
310
|
+
@closed = false
|
|
311
|
+
@event_count = 0
|
|
312
|
+
|
|
313
|
+
# Write header immediately
|
|
314
|
+
write_header
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Write an output event
|
|
318
|
+
#
|
|
319
|
+
# @param time [Float] Time offset in seconds
|
|
320
|
+
# @param data [String] Output data
|
|
321
|
+
# @return [void]
|
|
322
|
+
def write_output(time, data)
|
|
323
|
+
write_event(Event.new(time, EventType::OUTPUT, data))
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Write an input event
|
|
327
|
+
#
|
|
328
|
+
# @param time [Float] Time offset in seconds
|
|
329
|
+
# @param data [String] Input data
|
|
330
|
+
# @return [void]
|
|
331
|
+
def write_input(time, data)
|
|
332
|
+
write_event(Event.new(time, EventType::INPUT, data))
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Write a resize event
|
|
336
|
+
#
|
|
337
|
+
# @param time [Float] Time offset in seconds
|
|
338
|
+
# @param width [Integer] New width
|
|
339
|
+
# @param height [Integer] New height
|
|
340
|
+
# @return [void]
|
|
341
|
+
def write_resize(time, width, height)
|
|
342
|
+
write_event(Event.new(time, EventType::RESIZE, "#{width}x#{height}"))
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Write a marker event
|
|
346
|
+
#
|
|
347
|
+
# @param time [Float] Time offset in seconds
|
|
348
|
+
# @param label [String] Marker label
|
|
349
|
+
# @return [void]
|
|
350
|
+
def write_marker(time, label = "")
|
|
351
|
+
write_event(Event.new(time, EventType::MARKER, label))
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Write a generic event
|
|
355
|
+
#
|
|
356
|
+
# @param event [Event] Event to write
|
|
357
|
+
# @return [void]
|
|
358
|
+
def write_event(event)
|
|
359
|
+
raise RecordingError, "Writer is closed" if @closed
|
|
360
|
+
|
|
361
|
+
@io.puts(event.to_json)
|
|
362
|
+
@last_event_time = event.time
|
|
363
|
+
@event_count += 1
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Close the writer and finalize the recording
|
|
367
|
+
#
|
|
368
|
+
# @return [void]
|
|
369
|
+
def close
|
|
370
|
+
return if @closed
|
|
371
|
+
|
|
372
|
+
@closed = true
|
|
373
|
+
@io.flush if @io.respond_to?(:flush)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# @return [Boolean] Whether the writer is closed
|
|
377
|
+
def closed?
|
|
378
|
+
@closed
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# @return [Integer] Number of events written
|
|
382
|
+
def event_count
|
|
383
|
+
@event_count
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
private
|
|
387
|
+
|
|
388
|
+
# Write the header line
|
|
389
|
+
def write_header
|
|
390
|
+
@io.puts(@header.to_json)
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Reader for playing back asciicast recordings
|
|
395
|
+
#
|
|
396
|
+
# @example Read a recording
|
|
397
|
+
# File.open("recording.cast", "r") do |file|
|
|
398
|
+
# reader = Asciicast::Reader.new(file)
|
|
399
|
+
# puts "Recording: #{reader.header.title}"
|
|
400
|
+
# reader.each_event do |event|
|
|
401
|
+
# puts "#{event.time}: #{event.type}"
|
|
402
|
+
# end
|
|
403
|
+
# end
|
|
404
|
+
class Reader
|
|
405
|
+
# @return [Header] Recording header
|
|
406
|
+
attr_reader :header
|
|
407
|
+
|
|
408
|
+
# Create a new reader
|
|
409
|
+
#
|
|
410
|
+
# @param io [IO] Input stream
|
|
411
|
+
# @raise [FormatError] If header is invalid
|
|
412
|
+
def initialize(io)
|
|
413
|
+
@io = io
|
|
414
|
+
@header = read_header
|
|
415
|
+
@events_started = false
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# Get recording info from a file path
|
|
419
|
+
#
|
|
420
|
+
# @param path [String] Path to recording file
|
|
421
|
+
# @return [Hash] Recording metadata
|
|
422
|
+
# @raise [FormatError] If file is invalid
|
|
423
|
+
def self.info(path)
|
|
424
|
+
File.open(path, "r", encoding: "UTF-8") do |file|
|
|
425
|
+
reader = new(file)
|
|
426
|
+
header = reader.header
|
|
427
|
+
|
|
428
|
+
# Count events and calculate duration
|
|
429
|
+
event_count = 0
|
|
430
|
+
last_time = 0.0
|
|
431
|
+
|
|
432
|
+
reader.each_event do |event|
|
|
433
|
+
event_count += 1
|
|
434
|
+
last_time = event.time
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
{
|
|
438
|
+
version: header.version,
|
|
439
|
+
width: header.width,
|
|
440
|
+
height: header.height,
|
|
441
|
+
timestamp: header.timestamp,
|
|
442
|
+
duration: header.duration || last_time,
|
|
443
|
+
idle_time_limit: header.idle_time_limit,
|
|
444
|
+
command: header.command,
|
|
445
|
+
title: header.title,
|
|
446
|
+
env: header.env,
|
|
447
|
+
theme: header.theme,
|
|
448
|
+
event_count: event_count
|
|
449
|
+
}
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# Iterate over all events in the recording
|
|
454
|
+
#
|
|
455
|
+
# @yield [Event] Each event in order
|
|
456
|
+
# @return [Enumerator, void] If no block given, returns an Enumerator
|
|
457
|
+
def each_event
|
|
458
|
+
return enum_for(:each_event) unless block_given?
|
|
459
|
+
|
|
460
|
+
@events_started = true
|
|
461
|
+
|
|
462
|
+
@io.each_line do |line|
|
|
463
|
+
line = line.strip
|
|
464
|
+
next if line.empty?
|
|
465
|
+
|
|
466
|
+
begin
|
|
467
|
+
event = Event.from_json(line)
|
|
468
|
+
yield event
|
|
469
|
+
rescue FormatError
|
|
470
|
+
# Skip invalid lines (could be comments or garbage)
|
|
471
|
+
next
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# Read all events into an array
|
|
477
|
+
#
|
|
478
|
+
# @return [Array<Event>] All events
|
|
479
|
+
def events
|
|
480
|
+
each_event.to_a
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# Get total duration of the recording
|
|
484
|
+
#
|
|
485
|
+
# @return [Float] Duration in seconds
|
|
486
|
+
def duration
|
|
487
|
+
return @header.duration if @header.duration
|
|
488
|
+
|
|
489
|
+
# Calculate from events
|
|
490
|
+
last_time = 0.0
|
|
491
|
+
each_event { |e| last_time = e.time }
|
|
492
|
+
last_time
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
private
|
|
496
|
+
|
|
497
|
+
# Read and parse the header line
|
|
498
|
+
#
|
|
499
|
+
# @return [Header] Parsed header
|
|
500
|
+
# @raise [FormatError] If header is invalid
|
|
501
|
+
def read_header
|
|
502
|
+
line = @io.readline
|
|
503
|
+
Header.from_json(line.strip)
|
|
504
|
+
rescue EOFError
|
|
505
|
+
raise FormatError, "Empty file: no header found"
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
# Create a recording from a file path with a block
|
|
510
|
+
#
|
|
511
|
+
# @param path [String] Output file path
|
|
512
|
+
# @param width [Integer] Terminal width
|
|
513
|
+
# @param height [Integer] Terminal height
|
|
514
|
+
# @param kwargs [Hash] Additional header options
|
|
515
|
+
# @yield [Writer] Writer for adding events
|
|
516
|
+
# @return [void]
|
|
517
|
+
def self.create(path, width:, height:, **kwargs)
|
|
518
|
+
header = Header.new(width: width, height: height, **kwargs)
|
|
519
|
+
|
|
520
|
+
File.open(path, "w", encoding: "UTF-8") do |file|
|
|
521
|
+
writer = Writer.new(file, header)
|
|
522
|
+
yield writer if block_given?
|
|
523
|
+
writer.close
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
# Load a recording from a file path
|
|
528
|
+
#
|
|
529
|
+
# @param path [String] Input file path
|
|
530
|
+
# @return [Reader] Reader for the recording
|
|
531
|
+
# @raise [FormatError] If file is invalid
|
|
532
|
+
def self.load(path)
|
|
533
|
+
file = File.open(path, "r", encoding: "UTF-8")
|
|
534
|
+
Reader.new(file)
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
end
|