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,859 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Windows Console API bindings using Ruby's built-in Fiddle
|
|
4
|
+
# This module provides low-level access to Windows Console functions
|
|
5
|
+
# for terminal manipulation, ANSI support detection, and cursor control.
|
|
6
|
+
#
|
|
7
|
+
# Only loaded and functional on Windows platforms.
|
|
8
|
+
|
|
9
|
+
require "fiddle"
|
|
10
|
+
require "fiddle/import"
|
|
11
|
+
|
|
12
|
+
module Rich
|
|
13
|
+
module Win32Console
|
|
14
|
+
extend Fiddle::Importer
|
|
15
|
+
|
|
16
|
+
# Standard handle constants
|
|
17
|
+
STD_INPUT_HANDLE = -10
|
|
18
|
+
STD_OUTPUT_HANDLE = -11
|
|
19
|
+
STD_ERROR_HANDLE = -12
|
|
20
|
+
|
|
21
|
+
# Console mode flags
|
|
22
|
+
ENABLE_PROCESSED_INPUT = 0x0001
|
|
23
|
+
ENABLE_LINE_INPUT = 0x0002
|
|
24
|
+
ENABLE_ECHO_INPUT = 0x0004
|
|
25
|
+
ENABLE_WINDOW_INPUT = 0x0008
|
|
26
|
+
ENABLE_MOUSE_INPUT = 0x0010
|
|
27
|
+
ENABLE_INSERT_MODE = 0x0020
|
|
28
|
+
ENABLE_QUICK_EDIT_MODE = 0x0040
|
|
29
|
+
ENABLE_EXTENDED_FLAGS = 0x0080
|
|
30
|
+
ENABLE_AUTO_POSITION = 0x0100
|
|
31
|
+
ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
|
|
32
|
+
|
|
33
|
+
# Output mode flags
|
|
34
|
+
ENABLE_PROCESSED_OUTPUT = 0x0001
|
|
35
|
+
ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002
|
|
36
|
+
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
|
|
37
|
+
DISABLE_NEWLINE_AUTO_RETURN = 0x0008
|
|
38
|
+
ENABLE_LVB_GRID_WORLDWIDE = 0x0010
|
|
39
|
+
|
|
40
|
+
# Console text attributes (foreground colors)
|
|
41
|
+
FOREGROUND_BLUE = 0x0001
|
|
42
|
+
FOREGROUND_GREEN = 0x0002
|
|
43
|
+
FOREGROUND_RED = 0x0004
|
|
44
|
+
FOREGROUND_INTENSITY = 0x0008
|
|
45
|
+
|
|
46
|
+
# Console text attributes (background colors)
|
|
47
|
+
BACKGROUND_BLUE = 0x0010
|
|
48
|
+
BACKGROUND_GREEN = 0x0020
|
|
49
|
+
BACKGROUND_RED = 0x0040
|
|
50
|
+
BACKGROUND_INTENSITY = 0x0080
|
|
51
|
+
|
|
52
|
+
# Additional text attributes
|
|
53
|
+
COMMON_LVB_LEADING_BYTE = 0x0100
|
|
54
|
+
COMMON_LVB_TRAILING_BYTE = 0x0200
|
|
55
|
+
COMMON_LVB_GRID_HORIZONTAL = 0x0400
|
|
56
|
+
COMMON_LVB_GRID_LVERTICAL = 0x0800
|
|
57
|
+
COMMON_LVB_GRID_RVERTICAL = 0x1000
|
|
58
|
+
COMMON_LVB_REVERSE_VIDEO = 0x4000
|
|
59
|
+
COMMON_LVB_UNDERSCORE = 0x8000
|
|
60
|
+
|
|
61
|
+
# ANSI color number to Windows console attribute mapping
|
|
62
|
+
# Maps ANSI color indices (0-15) to Windows FOREGROUND/BACKGROUND values
|
|
63
|
+
ANSI_TO_WINDOWS_FG = [
|
|
64
|
+
0, # 0: Black
|
|
65
|
+
FOREGROUND_RED, # 1: Red
|
|
66
|
+
FOREGROUND_GREEN, # 2: Green
|
|
67
|
+
FOREGROUND_RED | FOREGROUND_GREEN, # 3: Yellow
|
|
68
|
+
FOREGROUND_BLUE, # 4: Blue
|
|
69
|
+
FOREGROUND_RED | FOREGROUND_BLUE, # 5: Magenta
|
|
70
|
+
FOREGROUND_GREEN | FOREGROUND_BLUE, # 6: Cyan
|
|
71
|
+
FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE, # 7: White
|
|
72
|
+
FOREGROUND_INTENSITY, # 8: Bright Black (Gray)
|
|
73
|
+
FOREGROUND_RED | FOREGROUND_INTENSITY, # 9: Bright Red
|
|
74
|
+
FOREGROUND_GREEN | FOREGROUND_INTENSITY, # 10: Bright Green
|
|
75
|
+
FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY, # 11: Bright Yellow
|
|
76
|
+
FOREGROUND_BLUE | FOREGROUND_INTENSITY, # 12: Bright Blue
|
|
77
|
+
FOREGROUND_RED | FOREGROUND_BLUE | FOREGROUND_INTENSITY, # 13: Bright Magenta
|
|
78
|
+
FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY, # 14: Bright Cyan
|
|
79
|
+
FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY # 15: Bright White
|
|
80
|
+
].freeze
|
|
81
|
+
|
|
82
|
+
ANSI_TO_WINDOWS_BG = [
|
|
83
|
+
0, # 0: Black
|
|
84
|
+
BACKGROUND_RED, # 1: Red
|
|
85
|
+
BACKGROUND_GREEN, # 2: Green
|
|
86
|
+
BACKGROUND_RED | BACKGROUND_GREEN, # 3: Yellow
|
|
87
|
+
BACKGROUND_BLUE, # 4: Blue
|
|
88
|
+
BACKGROUND_RED | BACKGROUND_BLUE, # 5: Magenta
|
|
89
|
+
BACKGROUND_GREEN | BACKGROUND_BLUE, # 6: Cyan
|
|
90
|
+
BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE, # 7: White
|
|
91
|
+
BACKGROUND_INTENSITY, # 8: Bright Black (Gray)
|
|
92
|
+
BACKGROUND_RED | BACKGROUND_INTENSITY, # 9: Bright Red
|
|
93
|
+
BACKGROUND_GREEN | BACKGROUND_INTENSITY, # 10: Bright Green
|
|
94
|
+
BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_INTENSITY, # 11: Bright Yellow
|
|
95
|
+
BACKGROUND_BLUE | BACKGROUND_INTENSITY, # 12: Bright Blue
|
|
96
|
+
BACKGROUND_RED | BACKGROUND_BLUE | BACKGROUND_INTENSITY, # 13: Bright Magenta
|
|
97
|
+
BACKGROUND_GREEN | BACKGROUND_BLUE | BACKGROUND_INTENSITY, # 14: Bright Cyan
|
|
98
|
+
BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE | BACKGROUND_INTENSITY # 15: Bright White
|
|
99
|
+
].freeze
|
|
100
|
+
|
|
101
|
+
if Gem.win_platform?
|
|
102
|
+
dlload "kernel32.dll"
|
|
103
|
+
|
|
104
|
+
# HANDLE WINAPI GetStdHandle(DWORD nStdHandle)
|
|
105
|
+
extern "void* GetStdHandle(unsigned long)"
|
|
106
|
+
|
|
107
|
+
# BOOL WINAPI GetConsoleMode(HANDLE hConsoleHandle, LPDWORD lpMode)
|
|
108
|
+
extern "int GetConsoleMode(void*, unsigned long*)"
|
|
109
|
+
|
|
110
|
+
# BOOL WINAPI SetConsoleMode(HANDLE hConsoleHandle, DWORD dwMode)
|
|
111
|
+
extern "int SetConsoleMode(void*, unsigned long)"
|
|
112
|
+
|
|
113
|
+
# BOOL WINAPI GetConsoleScreenBufferInfo(HANDLE hConsoleOutput, PCONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo)
|
|
114
|
+
extern "int GetConsoleScreenBufferInfo(void*, void*)"
|
|
115
|
+
|
|
116
|
+
# BOOL WINAPI SetConsoleCursorPosition(HANDLE hConsoleOutput, COORD dwCursorPosition)
|
|
117
|
+
extern "int SetConsoleCursorPosition(void*, unsigned long)"
|
|
118
|
+
|
|
119
|
+
# BOOL WINAPI SetConsoleTextAttribute(HANDLE hConsoleOutput, WORD wAttributes)
|
|
120
|
+
extern "int SetConsoleTextAttribute(void*, unsigned short)"
|
|
121
|
+
|
|
122
|
+
# BOOL WINAPI FillConsoleOutputCharacterW(HANDLE hConsoleOutput, WCHAR cCharacter, DWORD nLength, COORD dwWriteCoord, LPDWORD lpNumberOfCharsWritten)
|
|
123
|
+
extern "int FillConsoleOutputCharacterW(void*, unsigned short, unsigned long, unsigned long, unsigned long*)"
|
|
124
|
+
|
|
125
|
+
# BOOL WINAPI FillConsoleOutputAttribute(HANDLE hConsoleOutput, WORD wAttribute, DWORD nLength, COORD dwWriteCoord, LPDWORD lpNumberOfAttrsWritten)
|
|
126
|
+
extern "int FillConsoleOutputAttribute(void*, unsigned short, unsigned long, unsigned long, unsigned long*)"
|
|
127
|
+
|
|
128
|
+
# BOOL WINAPI SetConsoleTitleW(LPCWSTR lpConsoleTitle)
|
|
129
|
+
extern "int SetConsoleTitleW(void*)"
|
|
130
|
+
|
|
131
|
+
# BOOL WINAPI GetConsoleCursorInfo(HANDLE hConsoleOutput, PCONSOLE_CURSOR_INFO lpConsoleCursorInfo)
|
|
132
|
+
extern "int GetConsoleCursorInfo(void*, void*)"
|
|
133
|
+
|
|
134
|
+
# BOOL WINAPI SetConsoleCursorInfo(HANDLE hConsoleOutput, PCONSOLE_CURSOR_INFO lpConsoleCursorInfo)
|
|
135
|
+
extern "int SetConsoleCursorInfo(void*, void*)"
|
|
136
|
+
|
|
137
|
+
# BOOL WINAPI WriteConsoleW(HANDLE hConsoleOutput, CONST VOID* lpBuffer, DWORD nNumberOfCharsToWrite, LPDWORD lpNumberOfCharsWritten, LPVOID lpReserved)
|
|
138
|
+
extern "int WriteConsoleW(void*, void*, unsigned long, unsigned long*, void*)"
|
|
139
|
+
|
|
140
|
+
# BOOL WINAPI FlushConsoleInputBuffer(HANDLE hConsoleInput)
|
|
141
|
+
extern "int FlushConsoleInputBuffer(void*)"
|
|
142
|
+
|
|
143
|
+
# Screen buffer capture APIs for terminal recording
|
|
144
|
+
#
|
|
145
|
+
# BOOL WINAPI ReadConsoleOutputW(
|
|
146
|
+
# HANDLE hConsoleOutput, PCHAR_INFO lpBuffer, COORD dwBufferSize,
|
|
147
|
+
# COORD dwBufferCoord, PSMALL_RECT lpReadRegion)
|
|
148
|
+
extern "int ReadConsoleOutputW(void*, void*, unsigned long, unsigned long, void*)"
|
|
149
|
+
|
|
150
|
+
# BOOL WINAPI ReadConsoleOutputCharacterW(
|
|
151
|
+
# HANDLE hConsoleOutput, LPWSTR lpCharacter, DWORD nLength,
|
|
152
|
+
# COORD dwReadCoord, LPDWORD lpNumberOfCharsRead)
|
|
153
|
+
extern "int ReadConsoleOutputCharacterW(void*, void*, unsigned long, unsigned long, unsigned long*)"
|
|
154
|
+
|
|
155
|
+
# BOOL WINAPI ReadConsoleOutputAttribute(
|
|
156
|
+
# HANDLE hConsoleOutput, LPWORD lpAttribute, DWORD nLength,
|
|
157
|
+
# COORD dwReadCoord, LPDWORD lpNumberOfAttrsRead)
|
|
158
|
+
extern "int ReadConsoleOutputAttribute(void*, void*, unsigned long, unsigned long, unsigned long*)"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# CONSOLE_SCREEN_BUFFER_INFO structure layout:
|
|
162
|
+
# typedef struct _CONSOLE_SCREEN_BUFFER_INFO {
|
|
163
|
+
# COORD dwSize; // 4 bytes (2x SHORT)
|
|
164
|
+
# COORD dwCursorPosition; // 4 bytes (2x SHORT)
|
|
165
|
+
# WORD wAttributes; // 2 bytes
|
|
166
|
+
# SMALL_RECT srWindow; // 8 bytes (4x SHORT)
|
|
167
|
+
# COORD dwMaximumWindowSize; // 4 bytes (2x SHORT)
|
|
168
|
+
# } CONSOLE_SCREEN_BUFFER_INFO;
|
|
169
|
+
# Total: 22 bytes
|
|
170
|
+
CONSOLE_SCREEN_BUFFER_INFO_SIZE = 22
|
|
171
|
+
|
|
172
|
+
# CONSOLE_CURSOR_INFO structure layout:
|
|
173
|
+
# typedef struct _CONSOLE_CURSOR_INFO {
|
|
174
|
+
# DWORD dwSize; // 4 bytes
|
|
175
|
+
# BOOL bVisible; // 4 bytes
|
|
176
|
+
# } CONSOLE_CURSOR_INFO;
|
|
177
|
+
# Total: 8 bytes
|
|
178
|
+
CONSOLE_CURSOR_INFO_SIZE = 8
|
|
179
|
+
|
|
180
|
+
class << self
|
|
181
|
+
# @return [Boolean] Whether the current platform is Windows
|
|
182
|
+
def windows?
|
|
183
|
+
Gem.win_platform?
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# @return [Integer] Handle to stdout
|
|
187
|
+
def stdout_handle
|
|
188
|
+
return nil unless windows?
|
|
189
|
+
@stdout_handle ||= GetStdHandle(STD_OUTPUT_HANDLE)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# @return [Integer] Handle to stdin
|
|
193
|
+
def stdin_handle
|
|
194
|
+
return nil unless windows?
|
|
195
|
+
@stdin_handle ||= GetStdHandle(STD_INPUT_HANDLE)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# @return [Integer] Handle to stderr
|
|
199
|
+
def stderr_handle
|
|
200
|
+
return nil unless windows?
|
|
201
|
+
@stderr_handle ||= GetStdHandle(STD_ERROR_HANDLE)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Get the current console mode for a handle
|
|
205
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
206
|
+
# @return [Integer, nil] Console mode flags or nil on failure
|
|
207
|
+
def get_console_mode(handle = stdout_handle)
|
|
208
|
+
return nil unless windows? && handle
|
|
209
|
+
|
|
210
|
+
mode_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_LONG, Fiddle::RUBY_FREE)
|
|
211
|
+
result = GetConsoleMode(handle, mode_ptr)
|
|
212
|
+
return nil if result == 0
|
|
213
|
+
|
|
214
|
+
mode_ptr[0, Fiddle::SIZEOF_LONG].unpack1("L")
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Set the console mode for a handle
|
|
218
|
+
# @param mode [Integer] Console mode flags
|
|
219
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
220
|
+
# @return [Boolean] Success status
|
|
221
|
+
def set_console_mode(mode, handle = stdout_handle)
|
|
222
|
+
return false unless windows? && handle
|
|
223
|
+
|
|
224
|
+
SetConsoleMode(handle, mode) != 0
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Check if virtual terminal (ANSI) processing is supported
|
|
228
|
+
# @return [Boolean] True if ANSI escape sequences are supported
|
|
229
|
+
def supports_ansi?
|
|
230
|
+
return @supports_ansi if defined?(@supports_ansi)
|
|
231
|
+
|
|
232
|
+
unless windows?
|
|
233
|
+
@supports_ansi = true # Unix terminals support ANSI
|
|
234
|
+
return @supports_ansi
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
mode = get_console_mode
|
|
238
|
+
return @supports_ansi = false if mode.nil?
|
|
239
|
+
|
|
240
|
+
@supports_ansi = (mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Enable virtual terminal (ANSI) processing
|
|
244
|
+
# @return [Boolean] True if ANSI mode was successfully enabled
|
|
245
|
+
def enable_ansi!
|
|
246
|
+
return true unless windows? # Already supported on Unix
|
|
247
|
+
|
|
248
|
+
handle = stdout_handle
|
|
249
|
+
return false unless handle
|
|
250
|
+
|
|
251
|
+
current_mode = get_console_mode(handle)
|
|
252
|
+
return false unless current_mode
|
|
253
|
+
|
|
254
|
+
new_mode = current_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
|
255
|
+
result = set_console_mode(new_mode, handle)
|
|
256
|
+
|
|
257
|
+
# Update cached value
|
|
258
|
+
@supports_ansi = result
|
|
259
|
+
result
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Disable virtual terminal (ANSI) processing
|
|
263
|
+
# @return [Boolean] True if ANSI mode was successfully disabled
|
|
264
|
+
def disable_ansi!
|
|
265
|
+
return false unless windows?
|
|
266
|
+
|
|
267
|
+
handle = stdout_handle
|
|
268
|
+
return false unless handle
|
|
269
|
+
|
|
270
|
+
current_mode = get_console_mode(handle)
|
|
271
|
+
return false unless current_mode
|
|
272
|
+
|
|
273
|
+
new_mode = current_mode & ~ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
|
274
|
+
result = set_console_mode(new_mode, handle)
|
|
275
|
+
|
|
276
|
+
@supports_ansi = !result if result
|
|
277
|
+
result
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Get console screen buffer info
|
|
281
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
282
|
+
# @return [Hash, nil] Screen buffer info or nil on failure
|
|
283
|
+
def get_screen_buffer_info(handle = stdout_handle)
|
|
284
|
+
return nil unless windows? && handle
|
|
285
|
+
|
|
286
|
+
buffer = Fiddle::Pointer.malloc(CONSOLE_SCREEN_BUFFER_INFO_SIZE, Fiddle::RUBY_FREE)
|
|
287
|
+
result = GetConsoleScreenBufferInfo(handle, buffer)
|
|
288
|
+
return nil if result == 0
|
|
289
|
+
|
|
290
|
+
data = buffer[0, CONSOLE_SCREEN_BUFFER_INFO_SIZE]
|
|
291
|
+
|
|
292
|
+
# Unpack the structure
|
|
293
|
+
values = data.unpack("s2 s2 S s4 s2")
|
|
294
|
+
|
|
295
|
+
{
|
|
296
|
+
size: { width: values[0], height: values[1] },
|
|
297
|
+
cursor_position: { x: values[2], y: values[3] },
|
|
298
|
+
attributes: values[4],
|
|
299
|
+
window: {
|
|
300
|
+
left: values[5],
|
|
301
|
+
top: values[6],
|
|
302
|
+
right: values[7],
|
|
303
|
+
bottom: values[8]
|
|
304
|
+
},
|
|
305
|
+
max_window_size: { width: values[9], height: values[10] }
|
|
306
|
+
}
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Get the console window dimensions
|
|
310
|
+
# @return [Array<Integer>, nil] [width, height] or nil on failure
|
|
311
|
+
def get_size
|
|
312
|
+
return nil unless windows?
|
|
313
|
+
|
|
314
|
+
info = get_screen_buffer_info
|
|
315
|
+
return nil unless info
|
|
316
|
+
|
|
317
|
+
width = info[:window][:right] - info[:window][:left] + 1
|
|
318
|
+
height = info[:window][:bottom] - info[:window][:top] + 1
|
|
319
|
+
|
|
320
|
+
[width, height]
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Get current cursor position
|
|
324
|
+
# @return [Array<Integer>, nil] [x, y] or nil on failure
|
|
325
|
+
def get_cursor_position
|
|
326
|
+
return nil unless windows?
|
|
327
|
+
|
|
328
|
+
info = get_screen_buffer_info
|
|
329
|
+
return nil unless info
|
|
330
|
+
|
|
331
|
+
[info[:cursor_position][:x], info[:cursor_position][:y]]
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Set cursor position
|
|
335
|
+
# @param x [Integer] Column (0-indexed)
|
|
336
|
+
# @param y [Integer] Row (0-indexed)
|
|
337
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
338
|
+
# @return [Boolean] Success status
|
|
339
|
+
def set_cursor_position(x, y, handle = stdout_handle)
|
|
340
|
+
return false unless windows? && handle
|
|
341
|
+
|
|
342
|
+
# Pack COORD structure as DWORD (low word = X, high word = Y)
|
|
343
|
+
coord = (y << 16) | (x & 0xFFFF)
|
|
344
|
+
SetConsoleCursorPosition(handle, coord) != 0
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Set console text attributes (foreground/background colors)
|
|
348
|
+
# @param attributes [Integer] Attribute flags
|
|
349
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
350
|
+
# @return [Boolean] Success status
|
|
351
|
+
def set_text_attribute(attributes, handle = stdout_handle)
|
|
352
|
+
return false unless windows? && handle
|
|
353
|
+
|
|
354
|
+
SetConsoleTextAttribute(handle, attributes) != 0
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Get current text attributes
|
|
358
|
+
# @return [Integer, nil] Current attributes or nil on failure
|
|
359
|
+
def get_text_attributes
|
|
360
|
+
return nil unless windows?
|
|
361
|
+
|
|
362
|
+
info = get_screen_buffer_info
|
|
363
|
+
return nil unless info
|
|
364
|
+
|
|
365
|
+
info[:attributes]
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Fill console output with a character
|
|
369
|
+
# @param char [String] Character to fill with
|
|
370
|
+
# @param length [Integer] Number of cells to fill
|
|
371
|
+
# @param x [Integer] Starting column
|
|
372
|
+
# @param y [Integer] Starting row
|
|
373
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
374
|
+
# @return [Integer, nil] Number of characters written or nil on failure
|
|
375
|
+
def fill_output_character(char, length, x, y, handle = stdout_handle)
|
|
376
|
+
return nil unless windows? && handle
|
|
377
|
+
|
|
378
|
+
coord = (y << 16) | (x & 0xFFFF)
|
|
379
|
+
written_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_LONG, Fiddle::RUBY_FREE)
|
|
380
|
+
|
|
381
|
+
char_code = char.ord
|
|
382
|
+
result = FillConsoleOutputCharacterW(handle, char_code, length, coord, written_ptr)
|
|
383
|
+
return nil if result == 0
|
|
384
|
+
|
|
385
|
+
written_ptr[0, Fiddle::SIZEOF_LONG].unpack1("L")
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Fill console output with an attribute
|
|
389
|
+
# @param attribute [Integer] Attribute to fill with
|
|
390
|
+
# @param length [Integer] Number of cells to fill
|
|
391
|
+
# @param x [Integer] Starting column
|
|
392
|
+
# @param y [Integer] Starting row
|
|
393
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
394
|
+
# @return [Integer, nil] Number of cells written or nil on failure
|
|
395
|
+
def fill_output_attribute(attribute, length, x, y, handle = stdout_handle)
|
|
396
|
+
return nil unless windows? && handle
|
|
397
|
+
|
|
398
|
+
coord = (y << 16) | (x & 0xFFFF)
|
|
399
|
+
written_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_LONG, Fiddle::RUBY_FREE)
|
|
400
|
+
|
|
401
|
+
result = FillConsoleOutputAttribute(handle, attribute, length, coord, written_ptr)
|
|
402
|
+
return nil if result == 0
|
|
403
|
+
|
|
404
|
+
written_ptr[0, Fiddle::SIZEOF_LONG].unpack1("L")
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Set console window title
|
|
408
|
+
# @param title [String] New window title
|
|
409
|
+
# @return [Boolean] Success status
|
|
410
|
+
def set_title(title)
|
|
411
|
+
return false unless windows?
|
|
412
|
+
|
|
413
|
+
# Convert to UTF-16LE with null terminator
|
|
414
|
+
wide_title = (title + "\0").encode("UTF-16LE")
|
|
415
|
+
SetConsoleTitleW(Fiddle::Pointer[wide_title]) != 0
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# Show the cursor
|
|
419
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
420
|
+
# @return [Boolean] Success status
|
|
421
|
+
def show_cursor(handle = stdout_handle)
|
|
422
|
+
set_cursor_visibility(true, handle)
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# Hide the cursor
|
|
426
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
427
|
+
# @return [Boolean] Success status
|
|
428
|
+
def hide_cursor(handle = stdout_handle)
|
|
429
|
+
set_cursor_visibility(false, handle)
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Set cursor visibility
|
|
433
|
+
# @param visible [Boolean] Whether cursor should be visible
|
|
434
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
435
|
+
# @return [Boolean] Success status
|
|
436
|
+
def set_cursor_visibility(visible, handle = stdout_handle)
|
|
437
|
+
return false unless windows? && handle
|
|
438
|
+
|
|
439
|
+
# Get current cursor info
|
|
440
|
+
buffer = Fiddle::Pointer.malloc(CONSOLE_CURSOR_INFO_SIZE, Fiddle::RUBY_FREE)
|
|
441
|
+
result = GetConsoleCursorInfo(handle, buffer)
|
|
442
|
+
return false if result == 0
|
|
443
|
+
|
|
444
|
+
# Modify visibility
|
|
445
|
+
data = buffer[0, CONSOLE_CURSOR_INFO_SIZE].unpack("L L")
|
|
446
|
+
cursor_size = data[0]
|
|
447
|
+
buffer[0, CONSOLE_CURSOR_INFO_SIZE] = [cursor_size, visible ? 1 : 0].pack("L L")
|
|
448
|
+
|
|
449
|
+
SetConsoleCursorInfo(handle, buffer) != 0
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# Write text to console (bypassing Ruby's IO buffering)
|
|
453
|
+
# @param text [String] Text to write
|
|
454
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
455
|
+
# @return [Integer, nil] Number of characters written or nil on failure
|
|
456
|
+
def write_console(text, handle = stdout_handle)
|
|
457
|
+
return nil unless windows? && handle
|
|
458
|
+
|
|
459
|
+
wide_text = text.encode("UTF-16LE")
|
|
460
|
+
char_count = text.length
|
|
461
|
+
written_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_LONG, Fiddle::RUBY_FREE)
|
|
462
|
+
|
|
463
|
+
result = WriteConsoleW(handle, Fiddle::Pointer[wide_text], char_count, written_ptr, nil)
|
|
464
|
+
return nil if result == 0
|
|
465
|
+
|
|
466
|
+
written_ptr[0, Fiddle::SIZEOF_LONG].unpack1("L")
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# Clear the entire screen
|
|
470
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
471
|
+
# @return [Boolean] Success status
|
|
472
|
+
def clear_screen(handle = stdout_handle)
|
|
473
|
+
return false unless windows? && handle
|
|
474
|
+
|
|
475
|
+
info = get_screen_buffer_info(handle)
|
|
476
|
+
return false unless info
|
|
477
|
+
|
|
478
|
+
size = info[:size][:width] * info[:size][:height]
|
|
479
|
+
attributes = info[:attributes]
|
|
480
|
+
|
|
481
|
+
fill_output_character(" ", size, 0, 0, handle)
|
|
482
|
+
fill_output_attribute(attributes, size, 0, 0, handle)
|
|
483
|
+
set_cursor_position(0, 0, handle)
|
|
484
|
+
|
|
485
|
+
true
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# Erase from cursor to end of line
|
|
489
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
490
|
+
# @return [Boolean] Success status
|
|
491
|
+
def erase_line(handle = stdout_handle)
|
|
492
|
+
return false unless windows? && handle
|
|
493
|
+
|
|
494
|
+
info = get_screen_buffer_info(handle)
|
|
495
|
+
return false unless info
|
|
496
|
+
|
|
497
|
+
x = info[:cursor_position][:x]
|
|
498
|
+
y = info[:cursor_position][:y]
|
|
499
|
+
length = info[:size][:width] - x
|
|
500
|
+
attributes = info[:attributes]
|
|
501
|
+
|
|
502
|
+
fill_output_character(" ", length, x, y, handle)
|
|
503
|
+
fill_output_attribute(attributes, length, x, y, handle)
|
|
504
|
+
|
|
505
|
+
true
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# Move cursor up
|
|
509
|
+
# @param lines [Integer] Number of lines to move up
|
|
510
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
511
|
+
# @return [Boolean] Success status
|
|
512
|
+
def cursor_up(lines = 1, handle = stdout_handle)
|
|
513
|
+
return false unless windows?
|
|
514
|
+
|
|
515
|
+
pos = get_cursor_position
|
|
516
|
+
return false unless pos
|
|
517
|
+
|
|
518
|
+
new_y = [pos[1] - lines, 0].max
|
|
519
|
+
set_cursor_position(pos[0], new_y, handle)
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# Move cursor down
|
|
523
|
+
# @param lines [Integer] Number of lines to move down
|
|
524
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
525
|
+
# @return [Boolean] Success status
|
|
526
|
+
def cursor_down(lines = 1, handle = stdout_handle)
|
|
527
|
+
return false unless windows?
|
|
528
|
+
|
|
529
|
+
info = get_screen_buffer_info(handle)
|
|
530
|
+
return false unless info
|
|
531
|
+
|
|
532
|
+
pos = get_cursor_position
|
|
533
|
+
return false unless pos
|
|
534
|
+
|
|
535
|
+
max_y = info[:size][:height] - 1
|
|
536
|
+
new_y = [pos[1] + lines, max_y].min
|
|
537
|
+
set_cursor_position(pos[0], new_y, handle)
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
# Move cursor forward (right)
|
|
541
|
+
# @param columns [Integer] Number of columns to move
|
|
542
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
543
|
+
# @return [Boolean] Success status
|
|
544
|
+
def cursor_forward(columns = 1, handle = stdout_handle)
|
|
545
|
+
return false unless windows?
|
|
546
|
+
|
|
547
|
+
info = get_screen_buffer_info(handle)
|
|
548
|
+
return false unless info
|
|
549
|
+
|
|
550
|
+
pos = get_cursor_position
|
|
551
|
+
return false unless pos
|
|
552
|
+
|
|
553
|
+
max_x = info[:size][:width] - 1
|
|
554
|
+
new_x = [pos[0] + columns, max_x].min
|
|
555
|
+
set_cursor_position(new_x, pos[1], handle)
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
# Move cursor backward (left)
|
|
559
|
+
# @param columns [Integer] Number of columns to move
|
|
560
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
561
|
+
# @return [Boolean] Success status
|
|
562
|
+
def cursor_backward(columns = 1, handle = stdout_handle)
|
|
563
|
+
return false unless windows?
|
|
564
|
+
|
|
565
|
+
pos = get_cursor_position
|
|
566
|
+
return false unless pos
|
|
567
|
+
|
|
568
|
+
new_x = [pos[0] - columns, 0].max
|
|
569
|
+
set_cursor_position(new_x, pos[1], handle)
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
# Move cursor to the beginning of the line
|
|
573
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
574
|
+
# @return [Boolean] Success status
|
|
575
|
+
def cursor_to_column(column = 0, handle = stdout_handle)
|
|
576
|
+
return false unless windows?
|
|
577
|
+
|
|
578
|
+
pos = get_cursor_position
|
|
579
|
+
return false unless pos
|
|
580
|
+
|
|
581
|
+
set_cursor_position(column, pos[1], handle)
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# Convert ANSI color number to Windows console attributes
|
|
585
|
+
# @param foreground [Integer, nil] ANSI foreground color (0-15)
|
|
586
|
+
# @param background [Integer, nil] ANSI background color (0-15)
|
|
587
|
+
# @return [Integer] Windows console attribute value
|
|
588
|
+
def ansi_to_windows_attributes(foreground: nil, background: nil)
|
|
589
|
+
attributes = 0
|
|
590
|
+
attributes |= ANSI_TO_WINDOWS_FG[foreground] if foreground && foreground < 16
|
|
591
|
+
attributes |= ANSI_TO_WINDOWS_BG[background] if background && background < 16
|
|
592
|
+
attributes
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
# =========================================================================
|
|
596
|
+
# Screen Buffer Capture Methods (for terminal recording)
|
|
597
|
+
# =========================================================================
|
|
598
|
+
|
|
599
|
+
# CHAR_INFO structure:
|
|
600
|
+
# typedef struct _CHAR_INFO {
|
|
601
|
+
# union { WCHAR UnicodeChar; CHAR AsciiChar; } Char; // 2 bytes
|
|
602
|
+
# WORD Attributes; // 2 bytes
|
|
603
|
+
# } CHAR_INFO;
|
|
604
|
+
CHAR_INFO_SIZE = 4
|
|
605
|
+
|
|
606
|
+
# SMALL_RECT structure for region specification
|
|
607
|
+
SMALL_RECT_SIZE = 8
|
|
608
|
+
|
|
609
|
+
# Mapping from Windows console foreground attributes to ANSI color numbers
|
|
610
|
+
WINDOWS_FG_TO_ANSI = {
|
|
611
|
+
0 => 30, # Black
|
|
612
|
+
FOREGROUND_RED => 31, # Red
|
|
613
|
+
FOREGROUND_GREEN => 32, # Green
|
|
614
|
+
(FOREGROUND_RED | FOREGROUND_GREEN) => 33, # Yellow
|
|
615
|
+
FOREGROUND_BLUE => 34, # Blue
|
|
616
|
+
(FOREGROUND_RED | FOREGROUND_BLUE) => 35, # Magenta
|
|
617
|
+
(FOREGROUND_GREEN | FOREGROUND_BLUE) => 36, # Cyan
|
|
618
|
+
(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE) => 37 # White
|
|
619
|
+
}.freeze
|
|
620
|
+
|
|
621
|
+
# Mapping from Windows console background attributes to ANSI color numbers
|
|
622
|
+
WINDOWS_BG_TO_ANSI = {
|
|
623
|
+
0 => 40, # Black
|
|
624
|
+
BACKGROUND_RED => 41, # Red
|
|
625
|
+
BACKGROUND_GREEN => 42, # Green
|
|
626
|
+
(BACKGROUND_RED | BACKGROUND_GREEN) => 43, # Yellow
|
|
627
|
+
BACKGROUND_BLUE => 44, # Blue
|
|
628
|
+
(BACKGROUND_RED | BACKGROUND_BLUE) => 45, # Magenta
|
|
629
|
+
(BACKGROUND_GREEN | BACKGROUND_BLUE) => 46, # Cyan
|
|
630
|
+
(BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE) => 47 # White
|
|
631
|
+
}.freeze
|
|
632
|
+
|
|
633
|
+
# Read characters from a line in the screen buffer
|
|
634
|
+
#
|
|
635
|
+
# @param row [Integer] Row number (0-indexed)
|
|
636
|
+
# @param start_col [Integer] Starting column (0-indexed)
|
|
637
|
+
# @param length [Integer] Number of characters to read
|
|
638
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
639
|
+
# @return [String, nil] Characters read or nil on failure
|
|
640
|
+
def read_line_characters(row, start_col, length, handle = stdout_handle)
|
|
641
|
+
return nil unless windows? && handle
|
|
642
|
+
|
|
643
|
+
# Allocate buffer for UTF-16LE characters (2 bytes per char)
|
|
644
|
+
buffer = Fiddle::Pointer.malloc(length * 2, Fiddle::RUBY_FREE)
|
|
645
|
+
chars_read_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_LONG, Fiddle::RUBY_FREE)
|
|
646
|
+
|
|
647
|
+
# Pack COORD as DWORD (low word = X, high word = Y)
|
|
648
|
+
coord = (row << 16) | (start_col & 0xFFFF)
|
|
649
|
+
|
|
650
|
+
result = ReadConsoleOutputCharacterW(handle, buffer, length, coord, chars_read_ptr)
|
|
651
|
+
return nil if result == 0
|
|
652
|
+
|
|
653
|
+
chars_read = chars_read_ptr[0, Fiddle::SIZEOF_LONG].unpack1("L")
|
|
654
|
+
return "" if chars_read == 0
|
|
655
|
+
|
|
656
|
+
# Convert UTF-16LE to Ruby string
|
|
657
|
+
buffer[0, chars_read * 2].force_encoding("UTF-16LE").encode("UTF-8")
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
# Read attributes from a line in the screen buffer
|
|
661
|
+
#
|
|
662
|
+
# @param row [Integer] Row number (0-indexed)
|
|
663
|
+
# @param start_col [Integer] Starting column (0-indexed)
|
|
664
|
+
# @param length [Integer] Number of attributes to read
|
|
665
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
666
|
+
# @return [Array<Integer>, nil] Attribute values or nil on failure
|
|
667
|
+
def read_line_attributes(row, start_col, length, handle = stdout_handle)
|
|
668
|
+
return nil unless windows? && handle
|
|
669
|
+
|
|
670
|
+
# Allocate buffer for WORD attributes (2 bytes each)
|
|
671
|
+
buffer = Fiddle::Pointer.malloc(length * 2, Fiddle::RUBY_FREE)
|
|
672
|
+
attrs_read_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_LONG, Fiddle::RUBY_FREE)
|
|
673
|
+
|
|
674
|
+
# Pack COORD as DWORD
|
|
675
|
+
coord = (row << 16) | (start_col & 0xFFFF)
|
|
676
|
+
|
|
677
|
+
result = ReadConsoleOutputAttribute(handle, buffer, length, coord, attrs_read_ptr)
|
|
678
|
+
return nil if result == 0
|
|
679
|
+
|
|
680
|
+
attrs_read = attrs_read_ptr[0, Fiddle::SIZEOF_LONG].unpack1("L")
|
|
681
|
+
return [] if attrs_read == 0
|
|
682
|
+
|
|
683
|
+
# Unpack as array of unsigned shorts
|
|
684
|
+
buffer[0, attrs_read * 2].unpack("S*")
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
# Read a rectangular region from the screen buffer
|
|
688
|
+
#
|
|
689
|
+
# @param left [Integer] Left column (0-indexed)
|
|
690
|
+
# @param top [Integer] Top row (0-indexed)
|
|
691
|
+
# @param right [Integer] Right column (0-indexed, inclusive)
|
|
692
|
+
# @param bottom [Integer] Bottom row (0-indexed, inclusive)
|
|
693
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
694
|
+
# @return [Array<Array<Hash>>, nil] 2D array of {char:, attributes:} or nil on failure
|
|
695
|
+
def read_screen_region(left, top, right, bottom, handle = stdout_handle)
|
|
696
|
+
return nil unless windows? && handle
|
|
697
|
+
|
|
698
|
+
width = right - left + 1
|
|
699
|
+
height = bottom - top + 1
|
|
700
|
+
total_cells = width * height
|
|
701
|
+
|
|
702
|
+
# Allocate CHAR_INFO buffer
|
|
703
|
+
buffer = Fiddle::Pointer.malloc(total_cells * CHAR_INFO_SIZE, Fiddle::RUBY_FREE)
|
|
704
|
+
|
|
705
|
+
# Pack buffer size COORD (width, height)
|
|
706
|
+
buffer_size = (height << 16) | (width & 0xFFFF)
|
|
707
|
+
|
|
708
|
+
# Pack buffer coord (0, 0) - start of destination buffer
|
|
709
|
+
buffer_coord = 0
|
|
710
|
+
|
|
711
|
+
# Pack SMALL_RECT (left, top, right, bottom)
|
|
712
|
+
region_buffer = Fiddle::Pointer.malloc(SMALL_RECT_SIZE, Fiddle::RUBY_FREE)
|
|
713
|
+
region_buffer[0, SMALL_RECT_SIZE] = [left, top, right, bottom].pack("s4")
|
|
714
|
+
|
|
715
|
+
result = ReadConsoleOutputW(handle, buffer, buffer_size, buffer_coord, region_buffer)
|
|
716
|
+
return nil if result == 0
|
|
717
|
+
|
|
718
|
+
# Parse CHAR_INFO structures into 2D array
|
|
719
|
+
cells = []
|
|
720
|
+
height.times do |row|
|
|
721
|
+
row_cells = []
|
|
722
|
+
width.times do |col|
|
|
723
|
+
offset = (row * width + col) * CHAR_INFO_SIZE
|
|
724
|
+
char_data = buffer[offset, CHAR_INFO_SIZE]
|
|
725
|
+
|
|
726
|
+
# CHAR_INFO: 2 bytes unicode char, 2 bytes attributes
|
|
727
|
+
unicode_char, attributes = char_data.unpack("S S")
|
|
728
|
+
|
|
729
|
+
row_cells << {
|
|
730
|
+
char: [unicode_char].pack("U*"),
|
|
731
|
+
attributes: attributes
|
|
732
|
+
}
|
|
733
|
+
end
|
|
734
|
+
cells << row_cells
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
cells
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
# Capture the entire visible screen buffer
|
|
741
|
+
#
|
|
742
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
743
|
+
# @return [Hash, nil] Screen buffer data or nil on failure
|
|
744
|
+
# - :width [Integer] Screen width
|
|
745
|
+
# - :height [Integer] Screen height
|
|
746
|
+
# - :cursor_x [Integer] Cursor X position
|
|
747
|
+
# - :cursor_y [Integer] Cursor Y position
|
|
748
|
+
# - :lines [Array<Hash>] Array of line data
|
|
749
|
+
# - :chars [String] Characters in the line
|
|
750
|
+
# - :attributes [Array<Integer>] Attribute for each character
|
|
751
|
+
def capture_screen_buffer(handle = stdout_handle)
|
|
752
|
+
return nil unless windows? && handle
|
|
753
|
+
|
|
754
|
+
info = get_screen_buffer_info(handle)
|
|
755
|
+
return nil unless info
|
|
756
|
+
|
|
757
|
+
window = info[:window]
|
|
758
|
+
width = window[:right] - window[:left] + 1
|
|
759
|
+
height = window[:bottom] - window[:top] + 1
|
|
760
|
+
|
|
761
|
+
lines = []
|
|
762
|
+
height.times do |row|
|
|
763
|
+
actual_row = window[:top] + row
|
|
764
|
+
chars = read_line_characters(actual_row, window[:left], width, handle)
|
|
765
|
+
attributes = read_line_attributes(actual_row, window[:left], width, handle)
|
|
766
|
+
|
|
767
|
+
lines << {
|
|
768
|
+
chars: chars || "",
|
|
769
|
+
attributes: attributes || []
|
|
770
|
+
}
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
{
|
|
774
|
+
width: width,
|
|
775
|
+
height: height,
|
|
776
|
+
cursor_x: info[:cursor_position][:x] - window[:left],
|
|
777
|
+
cursor_y: info[:cursor_position][:y] - window[:top],
|
|
778
|
+
lines: lines
|
|
779
|
+
}
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
# Convert Windows console attributes to ANSI SGR codes
|
|
783
|
+
#
|
|
784
|
+
# @param attributes [Integer] Windows console attribute value
|
|
785
|
+
# @return [String] ANSI escape sequence for the given attributes
|
|
786
|
+
def windows_attr_to_ansi(attributes)
|
|
787
|
+
codes = []
|
|
788
|
+
|
|
789
|
+
# Extract foreground color (bits 0-3)
|
|
790
|
+
fg_color = attributes & 0x0F
|
|
791
|
+
fg_intense = (fg_color & FOREGROUND_INTENSITY) != 0
|
|
792
|
+
fg_base = fg_color & 0x07
|
|
793
|
+
|
|
794
|
+
# Map to ANSI foreground color
|
|
795
|
+
ansi_fg = WINDOWS_FG_TO_ANSI[fg_base]
|
|
796
|
+
ansi_fg += 60 if fg_intense && ansi_fg # Bright variant (90-97)
|
|
797
|
+
codes << ansi_fg if ansi_fg
|
|
798
|
+
|
|
799
|
+
# Extract background color (bits 4-7)
|
|
800
|
+
bg_color = (attributes >> 4) & 0x0F
|
|
801
|
+
bg_intense = (bg_color & 0x08) != 0
|
|
802
|
+
bg_base = (bg_color & 0x07) << 4 # Shift back to match WINDOWS_BG_TO_ANSI keys
|
|
803
|
+
|
|
804
|
+
# Map to ANSI background color
|
|
805
|
+
ansi_bg = WINDOWS_BG_TO_ANSI[bg_base]
|
|
806
|
+
ansi_bg += 60 if bg_intense && ansi_bg # Bright variant (100-107)
|
|
807
|
+
codes << ansi_bg if ansi_bg
|
|
808
|
+
|
|
809
|
+
# Handle other attributes
|
|
810
|
+
codes << 4 if (attributes & COMMON_LVB_UNDERSCORE) != 0 # Underline
|
|
811
|
+
codes << 7 if (attributes & COMMON_LVB_REVERSE_VIDEO) != 0 # Reverse
|
|
812
|
+
|
|
813
|
+
return "" if codes.empty?
|
|
814
|
+
|
|
815
|
+
"\e[#{codes.join(";")}m"
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
# Convert screen buffer capture to ANSI escape sequence string
|
|
819
|
+
#
|
|
820
|
+
# @param buffer_data [Hash] Data from capture_screen_buffer
|
|
821
|
+
# @return [String] ANSI escape sequence string representing the screen content
|
|
822
|
+
def buffer_to_ansi(buffer_data)
|
|
823
|
+
return "" unless buffer_data
|
|
824
|
+
|
|
825
|
+
output = StringIO.new
|
|
826
|
+
|
|
827
|
+
buffer_data[:lines].each_with_index do |line, row|
|
|
828
|
+
chars = line[:chars]
|
|
829
|
+
attributes = line[:attributes]
|
|
830
|
+
|
|
831
|
+
last_attr = nil
|
|
832
|
+
chars.each_char.with_index do |char, col|
|
|
833
|
+
attr = attributes[col] || 0
|
|
834
|
+
|
|
835
|
+
# Emit ANSI code if attributes changed
|
|
836
|
+
if attr != last_attr
|
|
837
|
+
output << "\e[0m" if last_attr # Reset first
|
|
838
|
+
ansi = windows_attr_to_ansi(attr)
|
|
839
|
+
output << ansi unless ansi.empty?
|
|
840
|
+
last_attr = attr
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
output << char
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
# Reset and newline (except for last line)
|
|
847
|
+
output << "\e[0m" if last_attr
|
|
848
|
+
output << "\r\n" if row < buffer_data[:lines].length - 1
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
output.string
|
|
852
|
+
end
|
|
853
|
+
end
|
|
854
|
+
|
|
855
|
+
# Auto-enable ANSI on Windows when this module is loaded
|
|
856
|
+
enable_ansi! if windows?
|
|
857
|
+
end
|
|
858
|
+
end
|
|
859
|
+
|