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.
@@ -0,0 +1,375 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+
5
+ module AsciinemaWin
6
+ # Represents a captured screen buffer state with delta detection capabilities
7
+ #
8
+ # This class captures the terminal screen buffer including:
9
+ # - Character content for each cell
10
+ # - Color/attribute information for each cell
11
+ # - Cursor position
12
+ # - Terminal dimensions
13
+ #
14
+ # @example Capture and compare screen states
15
+ # buffer1 = ScreenBuffer.capture
16
+ # # ... terminal content changes ...
17
+ # buffer2 = ScreenBuffer.capture
18
+ # diff = buffer2.diff(buffer1)
19
+ # puts diff # ANSI escape sequences to transform buffer1 to buffer2
20
+ class ScreenBuffer
21
+ # Cell data structure representing a single character cell
22
+ # @!attribute [r] char
23
+ # @return [String] The character in this cell
24
+ # @!attribute [r] foreground
25
+ # @return [Integer] Foreground color (ANSI color number 0-15 or 256-color/RGB)
26
+ # @!attribute [r] background
27
+ # @return [Integer] Background color (ANSI color number 0-15 or 256-color/RGB)
28
+ # @!attribute [r] attributes
29
+ # @return [Integer] Windows console attribute value (raw)
30
+ Cell = Data.define(:char, :foreground, :background, :attributes) do
31
+ # @return [Boolean] True if cell is empty (space with default colors)
32
+ def empty?
33
+ (char == " " || char == "\0") && foreground == 7 && background == 0
34
+ end
35
+
36
+ # @return [Boolean] True if this cell equals another
37
+ def ==(other)
38
+ return false unless other.is_a?(Cell)
39
+
40
+ char == other.char &&
41
+ foreground == other.foreground &&
42
+ background == other.background
43
+ end
44
+
45
+ alias eql? ==
46
+
47
+ # @return [Integer] Hash code for this cell
48
+ def hash
49
+ [char, foreground, background].hash
50
+ end
51
+ end
52
+
53
+ # @return [Integer] Screen width in characters
54
+ attr_reader :width
55
+
56
+ # @return [Integer] Screen height in characters
57
+ attr_reader :height
58
+
59
+ # @return [Integer] Cursor X position (0-indexed)
60
+ attr_reader :cursor_x
61
+
62
+ # @return [Integer] Cursor Y position (0-indexed)
63
+ attr_reader :cursor_y
64
+
65
+ # @return [Float] Timestamp when this buffer was captured (monotonic clock)
66
+ attr_reader :timestamp
67
+
68
+ # @return [Array<Array<Cell>>] 2D array of cells [row][col]
69
+ attr_reader :cells
70
+
71
+ # Create a new ScreenBuffer with the given dimensions
72
+ #
73
+ # @param width [Integer] Screen width
74
+ # @param height [Integer] Screen height
75
+ # @param cursor_x [Integer] Cursor X position
76
+ # @param cursor_y [Integer] Cursor Y position
77
+ # @param timestamp [Float, nil] Capture timestamp (defaults to current time)
78
+ def initialize(width:, height:, cursor_x: 0, cursor_y: 0, timestamp: nil)
79
+ @width = width
80
+ @height = height
81
+ @cursor_x = cursor_x
82
+ @cursor_y = cursor_y
83
+ @timestamp = timestamp || Process.clock_gettime(Process::CLOCK_MONOTONIC)
84
+ @cells = Array.new(height) { Array.new(width) { empty_cell } }
85
+ end
86
+
87
+ # Capture the current screen buffer state from the Windows console
88
+ #
89
+ # @return [ScreenBuffer, nil] Captured buffer or nil if capture fails
90
+ # @raise [PlatformError] If not running on Windows
91
+ def self.capture
92
+ unless Gem.win_platform?
93
+ raise PlatformError, "Screen buffer capture requires Windows"
94
+ end
95
+
96
+ unless defined?(Rich::Win32Console)
97
+ require_relative "../rich/win32_console"
98
+ end
99
+
100
+ buffer_data = Rich::Win32Console.capture_screen_buffer
101
+ return nil unless buffer_data
102
+
103
+ from_win32_data(buffer_data)
104
+ end
105
+
106
+ # Create a ScreenBuffer from Win32Console capture data
107
+ #
108
+ # @param data [Hash] Data from Win32Console.capture_screen_buffer
109
+ # @return [ScreenBuffer]
110
+ def self.from_win32_data(data)
111
+ buffer = new(
112
+ width: data[:width],
113
+ height: data[:height],
114
+ cursor_x: data[:cursor_x],
115
+ cursor_y: data[:cursor_y]
116
+ )
117
+
118
+ data[:lines].each_with_index do |line, row|
119
+ chars = line[:chars]
120
+ attributes = line[:attributes]
121
+
122
+ chars.each_char.with_index do |char, col|
123
+ break if col >= buffer.width
124
+
125
+ attr = attributes[col] || 0
126
+ fg, bg = parse_windows_attributes(attr)
127
+
128
+ buffer.set_cell(row, col, Cell.new(
129
+ char: char,
130
+ foreground: fg,
131
+ background: bg,
132
+ attributes: attr
133
+ ))
134
+ end
135
+ end
136
+
137
+ buffer
138
+ end
139
+
140
+ # Parse Windows console attributes into foreground and background colors
141
+ #
142
+ # @param attributes [Integer] Windows console attribute value
143
+ # @return [Array<Integer, Integer>] [foreground, background] ANSI color numbers
144
+ def self.parse_windows_attributes(attributes)
145
+ # Extract foreground (bits 0-3)
146
+ fg = attributes & 0x0F
147
+
148
+ # Extract background (bits 4-7)
149
+ bg = (attributes >> 4) & 0x0F
150
+
151
+ [fg, bg]
152
+ end
153
+
154
+ # Set a cell at the given position
155
+ #
156
+ # @param row [Integer] Row index (0-indexed)
157
+ # @param col [Integer] Column index (0-indexed)
158
+ # @param cell [Cell] Cell to set
159
+ # @return [void]
160
+ def set_cell(row, col, cell)
161
+ return if row < 0 || row >= @height || col < 0 || col >= @width
162
+
163
+ @cells[row][col] = cell
164
+ end
165
+
166
+ # Get a cell at the given position
167
+ #
168
+ # @param row [Integer] Row index (0-indexed)
169
+ # @param col [Integer] Column index (0-indexed)
170
+ # @return [Cell, nil] Cell at position or nil if out of bounds
171
+ def get_cell(row, col)
172
+ return nil if row < 0 || row >= @height || col < 0 || col >= @width
173
+
174
+ @cells[row][col]
175
+ end
176
+
177
+ # Compare this buffer with another and generate ANSI diff
178
+ #
179
+ # Produces the minimal ANSI escape sequence needed to transform
180
+ # the other buffer's display into this buffer's display.
181
+ #
182
+ # @param other [ScreenBuffer, nil] Previous buffer state (nil = empty screen)
183
+ # @return [String] ANSI escape sequences to apply the changes
184
+ def diff(other = nil)
185
+ output = StringIO.new
186
+
187
+ # If no previous buffer, render everything
188
+ if other.nil?
189
+ output << to_ansi
190
+ return output.string
191
+ end
192
+
193
+ # Track what needs to be updated
194
+ changes = []
195
+
196
+ @height.times do |row|
197
+ @width.times do |col|
198
+ current = get_cell(row, col)
199
+ previous = other.get_cell(row, col)
200
+
201
+ # Only emit changes
202
+ if current != previous
203
+ changes << { row: row, col: col, cell: current }
204
+ end
205
+ end
206
+ end
207
+
208
+ # If more than 50% changed, just redraw the whole screen
209
+ total_cells = @width * @height
210
+ if changes.length > total_cells / 2
211
+ output << "\e[H" # Move to home
212
+ output << to_ansi
213
+ return output.string
214
+ end
215
+
216
+ # Apply incremental changes
217
+ last_row = -1
218
+ last_col = -1
219
+ last_fg = nil
220
+ last_bg = nil
221
+
222
+ changes.each do |change|
223
+ row = change[:row]
224
+ col = change[:col]
225
+ cell = change[:cell]
226
+
227
+ # Move cursor if needed
228
+ if row != last_row || col != last_col + 1
229
+ output << "\e[#{row + 1};#{col + 1}H"
230
+ end
231
+
232
+ # Set colors if changed
233
+ if cell.foreground != last_fg || cell.background != last_bg
234
+ output << ansi_color_code(cell.foreground, cell.background)
235
+ last_fg = cell.foreground
236
+ last_bg = cell.background
237
+ end
238
+
239
+ output << cell.char
240
+
241
+ last_row = row
242
+ last_col = col
243
+ end
244
+
245
+ # Handle cursor position change
246
+ if @cursor_x != other.cursor_x || @cursor_y != other.cursor_y
247
+ output << "\e[#{@cursor_y + 1};#{@cursor_x + 1}H"
248
+ end
249
+
250
+ output.string
251
+ end
252
+
253
+ # Convert entire buffer to ANSI escape sequence string
254
+ #
255
+ # @return [String] Full ANSI representation of the screen buffer
256
+ def to_ansi
257
+ output = StringIO.new
258
+ last_fg = nil
259
+ last_bg = nil
260
+
261
+ @cells.each_with_index do |row_cells, row|
262
+ row_cells.each do |cell|
263
+ # Set colors if changed
264
+ if cell.foreground != last_fg || cell.background != last_bg
265
+ output << "\e[0m" if last_fg || last_bg # Reset first
266
+ output << ansi_color_code(cell.foreground, cell.background)
267
+ last_fg = cell.foreground
268
+ last_bg = cell.background
269
+ end
270
+
271
+ output << cell.char
272
+ end
273
+
274
+ # Newline between rows (except last)
275
+ if row < @height - 1
276
+ output << "\e[0m" if last_fg || last_bg # Reset before newline
277
+ output << "\r\n"
278
+ last_fg = nil
279
+ last_bg = nil
280
+ end
281
+ end
282
+
283
+ # Final reset
284
+ output << "\e[0m"
285
+
286
+ output.string
287
+ end
288
+
289
+ # Convert buffer to plain text (no color codes)
290
+ #
291
+ # @return [String] Plain text content of the buffer
292
+ def to_text
293
+ @cells.map { |row| row.map(&:char).join.rstrip }.join("\n").rstrip
294
+ end
295
+
296
+ # Check if this buffer equals another (content-wise)
297
+ #
298
+ # @param other [ScreenBuffer] Buffer to compare
299
+ # @return [Boolean] True if buffers have identical content
300
+ def ==(other)
301
+ return false unless other.is_a?(ScreenBuffer)
302
+ return false if @width != other.width || @height != other.height
303
+
304
+ @cells.each_with_index do |row_cells, row|
305
+ row_cells.each_with_index do |cell, col|
306
+ return false unless cell == other.get_cell(row, col)
307
+ end
308
+ end
309
+
310
+ true
311
+ end
312
+
313
+ alias eql? ==
314
+
315
+ # @return [Integer] Hash code for this buffer
316
+ def hash
317
+ [@width, @height, @cells].hash
318
+ end
319
+
320
+ # Check if buffer content has changed from another
321
+ #
322
+ # @param other [ScreenBuffer] Buffer to compare
323
+ # @return [Boolean] True if any content differs
324
+ def changed?(other)
325
+ self != other
326
+ end
327
+
328
+ private
329
+
330
+ # Create an empty cell with default colors
331
+ #
332
+ # @return [Cell] Empty cell
333
+ def empty_cell
334
+ Cell.new(char: " ", foreground: 7, background: 0, attributes: 7)
335
+ end
336
+
337
+ # Generate ANSI color code for foreground and background
338
+ #
339
+ # @param fg [Integer] Foreground color (0-15)
340
+ # @param bg [Integer] Background color (0-15)
341
+ # @return [String] ANSI escape sequence
342
+ def ansi_color_code(fg, bg)
343
+ codes = []
344
+
345
+ # Map Windows color to ANSI (0-7 standard, 8-15 bright)
346
+ if fg < 8
347
+ codes << (30 + WINDOWS_TO_ANSI_COLOR[fg])
348
+ else
349
+ codes << (90 + WINDOWS_TO_ANSI_COLOR[fg - 8])
350
+ end
351
+
352
+ if bg < 8
353
+ codes << (40 + WINDOWS_TO_ANSI_COLOR[bg])
354
+ else
355
+ codes << (100 + WINDOWS_TO_ANSI_COLOR[bg - 8])
356
+ end
357
+
358
+ "\e[#{codes.join(";")}m"
359
+ end
360
+
361
+ # Windows console colors use different bit ordering than ANSI
362
+ # Windows: BGR (Blue=1, Green=2, Red=4)
363
+ # ANSI: RGB (Red=1, Green=2, Blue=4)
364
+ WINDOWS_TO_ANSI_COLOR = [
365
+ 0, # 0: Black -> 0
366
+ 4, # 1: Blue -> 4
367
+ 2, # 2: Green -> 2
368
+ 6, # 3: Cyan -> 6
369
+ 1, # 4: Red -> 1
370
+ 5, # 5: Magenta -> 5
371
+ 3, # 6: Yellow -> 3
372
+ 7 # 7: White -> 7
373
+ ].freeze
374
+ end
375
+ end
@@ -0,0 +1,334 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsciinemaWin
4
+ # Terminal color themes for SVG/HTML export
5
+ #
6
+ # Provides color palettes for rendering terminal output in various
7
+ # popular themes like Dracula, Monokai, Solarized, etc.
8
+ module Themes
9
+ # Base theme structure
10
+ # @!attribute [r] name
11
+ # @return [String] Theme name
12
+ # @!attribute [r] background
13
+ # @return [String] Background color (hex)
14
+ # @!attribute [r] foreground
15
+ # @return [String] Default foreground color (hex)
16
+ # @!attribute [r] cursor
17
+ # @return [String] Cursor color (hex)
18
+ # @!attribute [r] palette
19
+ # @return [Array<String>] 16-color palette (8 normal + 8 bright)
20
+ Theme = Data.define(:name, :background, :foreground, :cursor, :palette) do
21
+ # Get color for ANSI color index
22
+ #
23
+ # @param index [Integer] ANSI color index (0-15 for basic, 16-255 for extended)
24
+ # @return [String] Hex color code
25
+ def color(index)
26
+ if index < 16
27
+ palette[index]
28
+ elsif index < 232
29
+ # 216 color cube (6x6x6)
30
+ index -= 16
31
+ r = (index / 36) % 6
32
+ g = (index / 6) % 6
33
+ b = index % 6
34
+ r_val = r == 0 ? 0 : 55 + r * 40
35
+ g_val = g == 0 ? 0 : 55 + g * 40
36
+ b_val = b == 0 ? 0 : 55 + b * 40
37
+ format("#%02x%02x%02x", r_val, g_val, b_val)
38
+ else
39
+ # 24 grayscale colors
40
+ gray = (index - 232) * 10 + 8
41
+ format("#%02x%02x%02x", gray, gray, gray)
42
+ end
43
+ end
44
+
45
+ # Get foreground color for ANSI code
46
+ #
47
+ # @param code [Integer] ANSI SGR foreground code (30-37, 90-97)
48
+ # @return [String] Hex color code
49
+ def fg_color(code)
50
+ case code
51
+ when 30..37 then palette[code - 30]
52
+ when 90..97 then palette[code - 90 + 8]
53
+ when 39 then foreground
54
+ else foreground
55
+ end
56
+ end
57
+
58
+ # Get background color for ANSI code
59
+ #
60
+ # @param code [Integer] ANSI SGR background code (40-47, 100-107)
61
+ # @return [String] Hex color code
62
+ def bg_color(code)
63
+ case code
64
+ when 40..47 then palette[code - 40]
65
+ when 100..107 then palette[code - 100 + 8]
66
+ when 49 then background
67
+ else background
68
+ end
69
+ end
70
+ end
71
+
72
+ # Default asciinema theme
73
+ ASCIINEMA = Theme.new(
74
+ name: "asciinema",
75
+ background: "#121314",
76
+ foreground: "#cccccc",
77
+ cursor: "#cccccc",
78
+ palette: [
79
+ "#000000", # Black
80
+ "#dd3c69", # Red
81
+ "#4ebf22", # Green
82
+ "#ddaf3c", # Yellow
83
+ "#26b0d7", # Blue
84
+ "#b954e1", # Magenta
85
+ "#54e1b9", # Cyan
86
+ "#d9d9d9", # White
87
+ "#4d4d4d", # Bright Black
88
+ "#dd3c69", # Bright Red
89
+ "#4ebf22", # Bright Green
90
+ "#ddaf3c", # Bright Yellow
91
+ "#26b0d7", # Bright Blue
92
+ "#b954e1", # Bright Magenta
93
+ "#54e1b9", # Bright Cyan
94
+ "#ffffff" # Bright White
95
+ ]
96
+ ).freeze
97
+
98
+ # Dracula theme
99
+ DRACULA = Theme.new(
100
+ name: "dracula",
101
+ background: "#282a36",
102
+ foreground: "#f8f8f2",
103
+ cursor: "#f8f8f2",
104
+ palette: [
105
+ "#21222c", # Black
106
+ "#ff5555", # Red
107
+ "#50fa7b", # Green
108
+ "#f1fa8c", # Yellow
109
+ "#bd93f9", # Blue
110
+ "#ff79c6", # Magenta
111
+ "#8be9fd", # Cyan
112
+ "#f8f8f2", # White
113
+ "#6272a4", # Bright Black
114
+ "#ff6e6e", # Bright Red
115
+ "#69ff94", # Bright Green
116
+ "#ffffa5", # Bright Yellow
117
+ "#d6acff", # Bright Blue
118
+ "#ff92df", # Bright Magenta
119
+ "#a4ffff", # Bright Cyan
120
+ "#ffffff" # Bright White
121
+ ]
122
+ ).freeze
123
+
124
+ # Monokai theme
125
+ MONOKAI = Theme.new(
126
+ name: "monokai",
127
+ background: "#272822",
128
+ foreground: "#f8f8f2",
129
+ cursor: "#f8f8f2",
130
+ palette: [
131
+ "#272822", # Black
132
+ "#f92672", # Red
133
+ "#a6e22e", # Green
134
+ "#f4bf75", # Yellow
135
+ "#66d9ef", # Blue
136
+ "#ae81ff", # Magenta
137
+ "#a1efe4", # Cyan
138
+ "#f8f8f2", # White
139
+ "#75715e", # Bright Black
140
+ "#f92672", # Bright Red
141
+ "#a6e22e", # Bright Green
142
+ "#f4bf75", # Bright Yellow
143
+ "#66d9ef", # Bright Blue
144
+ "#ae81ff", # Bright Magenta
145
+ "#a1efe4", # Bright Cyan
146
+ "#f9f8f5" # Bright White
147
+ ]
148
+ ).freeze
149
+
150
+ # Solarized Dark theme
151
+ SOLARIZED_DARK = Theme.new(
152
+ name: "solarized-dark",
153
+ background: "#002b36",
154
+ foreground: "#839496",
155
+ cursor: "#839496",
156
+ palette: [
157
+ "#073642", # Black
158
+ "#dc322f", # Red
159
+ "#859900", # Green
160
+ "#b58900", # Yellow
161
+ "#268bd2", # Blue
162
+ "#d33682", # Magenta
163
+ "#2aa198", # Cyan
164
+ "#eee8d5", # White
165
+ "#002b36", # Bright Black
166
+ "#cb4b16", # Bright Red
167
+ "#586e75", # Bright Green
168
+ "#657b83", # Bright Yellow
169
+ "#839496", # Bright Blue
170
+ "#6c71c4", # Bright Magenta
171
+ "#93a1a1", # Bright Cyan
172
+ "#fdf6e3" # Bright White
173
+ ]
174
+ ).freeze
175
+
176
+ # Solarized Light theme
177
+ SOLARIZED_LIGHT = Theme.new(
178
+ name: "solarized-light",
179
+ background: "#fdf6e3",
180
+ foreground: "#657b83",
181
+ cursor: "#657b83",
182
+ palette: [
183
+ "#073642", # Black
184
+ "#dc322f", # Red
185
+ "#859900", # Green
186
+ "#b58900", # Yellow
187
+ "#268bd2", # Blue
188
+ "#d33682", # Magenta
189
+ "#2aa198", # Cyan
190
+ "#eee8d5", # White
191
+ "#002b36", # Bright Black
192
+ "#cb4b16", # Bright Red
193
+ "#586e75", # Bright Green
194
+ "#657b83", # Bright Yellow
195
+ "#839496", # Bright Blue
196
+ "#6c71c4", # Bright Magenta
197
+ "#93a1a1", # Bright Cyan
198
+ "#fdf6e3" # Bright White
199
+ ]
200
+ ).freeze
201
+
202
+ # Nord theme
203
+ NORD = Theme.new(
204
+ name: "nord",
205
+ background: "#2e3440",
206
+ foreground: "#d8dee9",
207
+ cursor: "#d8dee9",
208
+ palette: [
209
+ "#3b4252", # Black
210
+ "#bf616a", # Red
211
+ "#a3be8c", # Green
212
+ "#ebcb8b", # Yellow
213
+ "#81a1c1", # Blue
214
+ "#b48ead", # Magenta
215
+ "#88c0d0", # Cyan
216
+ "#e5e9f0", # White
217
+ "#4c566a", # Bright Black
218
+ "#bf616a", # Bright Red
219
+ "#a3be8c", # Bright Green
220
+ "#ebcb8b", # Bright Yellow
221
+ "#81a1c1", # Bright Blue
222
+ "#b48ead", # Bright Magenta
223
+ "#8fbcbb", # Bright Cyan
224
+ "#eceff4" # Bright White
225
+ ]
226
+ ).freeze
227
+
228
+ # One Dark theme (Atom)
229
+ ONE_DARK = Theme.new(
230
+ name: "one-dark",
231
+ background: "#282c34",
232
+ foreground: "#abb2bf",
233
+ cursor: "#528bff",
234
+ palette: [
235
+ "#282c34", # Black
236
+ "#e06c75", # Red
237
+ "#98c379", # Green
238
+ "#e5c07b", # Yellow
239
+ "#61afef", # Blue
240
+ "#c678dd", # Magenta
241
+ "#56b6c2", # Cyan
242
+ "#abb2bf", # White
243
+ "#545862", # Bright Black
244
+ "#e06c75", # Bright Red
245
+ "#98c379", # Bright Green
246
+ "#e5c07b", # Bright Yellow
247
+ "#61afef", # Bright Blue
248
+ "#c678dd", # Bright Magenta
249
+ "#56b6c2", # Bright Cyan
250
+ "#c8ccd4" # Bright White
251
+ ]
252
+ ).freeze
253
+
254
+ # GitHub Dark theme
255
+ GITHUB_DARK = Theme.new(
256
+ name: "github-dark",
257
+ background: "#0d1117",
258
+ foreground: "#c9d1d9",
259
+ cursor: "#c9d1d9",
260
+ palette: [
261
+ "#484f58", # Black
262
+ "#ff7b72", # Red
263
+ "#3fb950", # Green
264
+ "#d29922", # Yellow
265
+ "#58a6ff", # Blue
266
+ "#bc8cff", # Magenta
267
+ "#39c5cf", # Cyan
268
+ "#b1bac4", # White
269
+ "#6e7681", # Bright Black
270
+ "#ffa198", # Bright Red
271
+ "#56d364", # Bright Green
272
+ "#e3b341", # Bright Yellow
273
+ "#79c0ff", # Bright Blue
274
+ "#d2a8ff", # Bright Magenta
275
+ "#56d4dd", # Bright Cyan
276
+ "#f0f6fc" # Bright White
277
+ ]
278
+ ).freeze
279
+
280
+ # Tokyo Night theme
281
+ TOKYO_NIGHT = Theme.new(
282
+ name: "tokyo-night",
283
+ background: "#1a1b26",
284
+ foreground: "#a9b1d6",
285
+ cursor: "#c0caf5",
286
+ palette: [
287
+ "#15161e", # Black
288
+ "#f7768e", # Red
289
+ "#9ece6a", # Green
290
+ "#e0af68", # Yellow
291
+ "#7aa2f7", # Blue
292
+ "#bb9af7", # Magenta
293
+ "#7dcfff", # Cyan
294
+ "#a9b1d6", # White
295
+ "#414868", # Bright Black
296
+ "#f7768e", # Bright Red
297
+ "#9ece6a", # Bright Green
298
+ "#e0af68", # Bright Yellow
299
+ "#7aa2f7", # Bright Blue
300
+ "#bb9af7", # Bright Magenta
301
+ "#7dcfff", # Bright Cyan
302
+ "#c0caf5" # Bright White
303
+ ]
304
+ ).freeze
305
+
306
+ # All available themes
307
+ ALL = {
308
+ "asciinema" => ASCIINEMA,
309
+ "dracula" => DRACULA,
310
+ "monokai" => MONOKAI,
311
+ "solarized-dark" => SOLARIZED_DARK,
312
+ "solarized-light" => SOLARIZED_LIGHT,
313
+ "nord" => NORD,
314
+ "one-dark" => ONE_DARK,
315
+ "github-dark" => GITHUB_DARK,
316
+ "tokyo-night" => TOKYO_NIGHT
317
+ }.freeze
318
+
319
+ # Get theme by name
320
+ #
321
+ # @param name [String] Theme name
322
+ # @return [Theme] Theme or default if not found
323
+ def self.get(name)
324
+ ALL[name.to_s.downcase] || ASCIINEMA
325
+ end
326
+
327
+ # List available theme names
328
+ #
329
+ # @return [Array<String>] Theme names
330
+ def self.names
331
+ ALL.keys
332
+ end
333
+ end
334
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AsciinemaWin
4
+ # @return [String] Gem version following semantic versioning
5
+ VERSION = "0.1.0"
6
+ end