rich-ruby 1.0.1 → 1.0.2
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 +4 -4
- data/CHANGELOG.md +80 -0
- data/LICENSE +21 -21
- data/README.md +547 -547
- data/docs/architecture.md +43 -0
- data/docs/cheat-sheet.md +52 -0
- data/docs/customization.md +53 -0
- data/docs/how-to-use.md +96 -0
- data/docs/test-report.md +112 -0
- data/docs/troubleshooting.md +36 -0
- data/docs/windows-notes.md +30 -0
- data/examples/demo.rb +106 -106
- data/examples/showcase.rb +420 -420
- data/examples/smoke_test.rb +41 -41
- data/examples/stress_test.rb +604 -604
- data/examples/syntax_markdown_demo.rb +166 -166
- data/examples/verify.rb +216 -215
- data/examples/visual_demo.rb +145 -145
- data/lib/rich/_palettes.rb +148 -148
- data/lib/rich/box.rb +342 -342
- data/lib/rich/cells.rb +524 -512
- data/lib/rich/color.rb +631 -628
- data/lib/rich/color_triplet.rb +227 -220
- data/lib/rich/console.rb +604 -549
- data/lib/rich/control.rb +332 -332
- data/lib/rich/json.rb +260 -254
- data/lib/rich/layout.rb +314 -314
- data/lib/rich/markdown.rb +531 -509
- data/lib/rich/markup.rb +186 -175
- data/lib/rich/panel.rb +318 -311
- data/lib/rich/progress.rb +430 -430
- data/lib/rich/segment.rb +387 -387
- data/lib/rich/style.rb +464 -433
- data/lib/rich/syntax.rb +1220 -1145
- data/lib/rich/table.rb +547 -525
- data/lib/rich/terminal_theme.rb +126 -126
- data/lib/rich/text.rb +460 -433
- data/lib/rich/tree.rb +220 -220
- data/lib/rich/version.rb +5 -5
- data/lib/rich/win32_console.rb +620 -582
- data/lib/rich.rb +108 -108
- metadata +15 -5
data/lib/rich/win32_console.rb
CHANGED
|
@@ -1,582 +1,620 @@
|
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
#
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
#
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
#
|
|
156
|
-
#
|
|
157
|
-
#
|
|
158
|
-
#
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# @return [Integer] Handle to
|
|
176
|
-
def
|
|
177
|
-
return nil unless windows?
|
|
178
|
-
@
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
# @return [Integer] Handle to
|
|
182
|
-
def
|
|
183
|
-
return nil unless windows?
|
|
184
|
-
@
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
#
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
#
|
|
201
|
-
#
|
|
202
|
-
#
|
|
203
|
-
#
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
#
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
return false unless handle
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
return false
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
#
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
return nil
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
#
|
|
320
|
-
# @
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
#
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
#
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
#
|
|
409
|
-
# @
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
#
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
set_cursor_position(
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
#
|
|
508
|
-
# @
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
set_cursor_position(
|
|
539
|
-
end
|
|
540
|
-
|
|
541
|
-
# Move cursor
|
|
542
|
-
# @param
|
|
543
|
-
# @param handle [Integer] Console handle (defaults to stdout)
|
|
544
|
-
# @return [Boolean] Success status
|
|
545
|
-
def
|
|
546
|
-
return false unless windows?
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
return false unless
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
+
# DWORD WINAPI GetLastError(void)
|
|
144
|
+
extern "unsigned long GetLastError()"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Sentinel returned by GetStdHandle on failure.
|
|
148
|
+
INVALID_HANDLE_VALUE = (1 << (Fiddle::SIZEOF_VOIDP * 8)) - 1
|
|
149
|
+
|
|
150
|
+
# CONSOLE_SCREEN_BUFFER_INFO structure layout:
|
|
151
|
+
# typedef struct _CONSOLE_SCREEN_BUFFER_INFO {
|
|
152
|
+
# COORD dwSize; // 4 bytes (2x SHORT)
|
|
153
|
+
# COORD dwCursorPosition; // 4 bytes (2x SHORT)
|
|
154
|
+
# WORD wAttributes; // 2 bytes
|
|
155
|
+
# SMALL_RECT srWindow; // 8 bytes (4x SHORT)
|
|
156
|
+
# COORD dwMaximumWindowSize; // 4 bytes (2x SHORT)
|
|
157
|
+
# } CONSOLE_SCREEN_BUFFER_INFO;
|
|
158
|
+
# Total: 22 bytes
|
|
159
|
+
CONSOLE_SCREEN_BUFFER_INFO_SIZE = 22
|
|
160
|
+
|
|
161
|
+
# CONSOLE_CURSOR_INFO structure layout:
|
|
162
|
+
# typedef struct _CONSOLE_CURSOR_INFO {
|
|
163
|
+
# DWORD dwSize; // 4 bytes
|
|
164
|
+
# BOOL bVisible; // 4 bytes
|
|
165
|
+
# } CONSOLE_CURSOR_INFO;
|
|
166
|
+
# Total: 8 bytes
|
|
167
|
+
CONSOLE_CURSOR_INFO_SIZE = 8
|
|
168
|
+
|
|
169
|
+
class << self
|
|
170
|
+
# @return [Boolean] Whether the current platform is Windows
|
|
171
|
+
def windows?
|
|
172
|
+
Gem.win_platform?
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# @return [Integer, nil] Handle to stdout, or nil if unavailable
|
|
176
|
+
def stdout_handle
|
|
177
|
+
return nil unless windows?
|
|
178
|
+
@stdout_handle ||= valid_handle(GetStdHandle(STD_OUTPUT_HANDLE))
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# @return [Integer, nil] Handle to stdin, or nil if unavailable
|
|
182
|
+
def stdin_handle
|
|
183
|
+
return nil unless windows?
|
|
184
|
+
@stdin_handle ||= valid_handle(GetStdHandle(STD_INPUT_HANDLE))
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# @return [Integer, nil] Handle to stderr, or nil if unavailable
|
|
188
|
+
def stderr_handle
|
|
189
|
+
return nil unless windows?
|
|
190
|
+
@stderr_handle ||= valid_handle(GetStdHandle(STD_ERROR_HANDLE))
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# @return [Integer] The calling thread's last Win32 error code (0 if none)
|
|
194
|
+
def last_error
|
|
195
|
+
return 0 unless windows?
|
|
196
|
+
|
|
197
|
+
GetLastError()
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Normalize a raw GetStdHandle result: NULL (0) means "no such handle"
|
|
201
|
+
# (e.g. output redirected to a pipe in a GUI process) and
|
|
202
|
+
# INVALID_HANDLE_VALUE means an error. Both become nil so callers and the
|
|
203
|
+
# `&& handle` guards short-circuit instead of issuing API calls against a
|
|
204
|
+
# bad handle. A failed lookup is intentionally NOT memoized.
|
|
205
|
+
def valid_handle(handle)
|
|
206
|
+
return nil if handle.nil?
|
|
207
|
+
|
|
208
|
+
value = handle.respond_to?(:to_i) ? handle.to_i : handle
|
|
209
|
+
return nil if value.zero? || value == INVALID_HANDLE_VALUE
|
|
210
|
+
|
|
211
|
+
handle
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Get the current console mode for a handle
|
|
215
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
216
|
+
# @return [Integer, nil] Console mode flags or nil on failure
|
|
217
|
+
def get_console_mode(handle = stdout_handle)
|
|
218
|
+
return nil unless windows? && handle
|
|
219
|
+
|
|
220
|
+
mode_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_LONG, Fiddle::RUBY_FREE)
|
|
221
|
+
result = GetConsoleMode(handle, mode_ptr)
|
|
222
|
+
return nil if result == 0
|
|
223
|
+
|
|
224
|
+
mode_ptr[0, Fiddle::SIZEOF_LONG].unpack1("L")
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Set the console mode for a handle
|
|
228
|
+
# @param mode [Integer] Console mode flags
|
|
229
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
230
|
+
# @return [Boolean] Success status
|
|
231
|
+
def set_console_mode(mode, handle = stdout_handle)
|
|
232
|
+
return false unless windows? && handle
|
|
233
|
+
|
|
234
|
+
SetConsoleMode(handle, mode) != 0
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Check if virtual terminal (ANSI) processing is supported
|
|
238
|
+
# @return [Boolean] True if ANSI escape sequences are supported
|
|
239
|
+
def supports_ansi?
|
|
240
|
+
return @supports_ansi if defined?(@supports_ansi)
|
|
241
|
+
|
|
242
|
+
unless windows?
|
|
243
|
+
@supports_ansi = true # Unix terminals support ANSI
|
|
244
|
+
return @supports_ansi
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
mode = get_console_mode
|
|
248
|
+
return @supports_ansi = false if mode.nil?
|
|
249
|
+
|
|
250
|
+
@supports_ansi = (mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Enable virtual terminal (ANSI) processing
|
|
254
|
+
# @return [Boolean] True if ANSI mode was successfully enabled
|
|
255
|
+
def enable_ansi!
|
|
256
|
+
return true unless windows? # Already supported on Unix
|
|
257
|
+
|
|
258
|
+
handle = stdout_handle
|
|
259
|
+
return false unless handle
|
|
260
|
+
|
|
261
|
+
current_mode = get_console_mode(handle)
|
|
262
|
+
return false unless current_mode
|
|
263
|
+
|
|
264
|
+
new_mode = current_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
|
265
|
+
result = set_console_mode(new_mode, handle)
|
|
266
|
+
|
|
267
|
+
# Update cached value
|
|
268
|
+
@supports_ansi = result
|
|
269
|
+
result
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Disable virtual terminal (ANSI) processing
|
|
273
|
+
# @return [Boolean] True if ANSI mode was successfully disabled
|
|
274
|
+
def disable_ansi!
|
|
275
|
+
return false unless windows?
|
|
276
|
+
|
|
277
|
+
handle = stdout_handle
|
|
278
|
+
return false unless handle
|
|
279
|
+
|
|
280
|
+
current_mode = get_console_mode(handle)
|
|
281
|
+
return false unless current_mode
|
|
282
|
+
|
|
283
|
+
new_mode = current_mode & ~ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
|
284
|
+
result = set_console_mode(new_mode, handle)
|
|
285
|
+
|
|
286
|
+
@supports_ansi = !result if result
|
|
287
|
+
result
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Get console screen buffer info
|
|
291
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
292
|
+
# @return [Hash, nil] Screen buffer info or nil on failure
|
|
293
|
+
def get_screen_buffer_info(handle = stdout_handle)
|
|
294
|
+
return nil unless windows? && handle
|
|
295
|
+
|
|
296
|
+
buffer = Fiddle::Pointer.malloc(CONSOLE_SCREEN_BUFFER_INFO_SIZE, Fiddle::RUBY_FREE)
|
|
297
|
+
result = GetConsoleScreenBufferInfo(handle, buffer)
|
|
298
|
+
return nil if result == 0
|
|
299
|
+
|
|
300
|
+
data = buffer[0, CONSOLE_SCREEN_BUFFER_INFO_SIZE]
|
|
301
|
+
|
|
302
|
+
# Unpack the structure
|
|
303
|
+
values = data.unpack("s2 s2 S s4 s2")
|
|
304
|
+
|
|
305
|
+
{
|
|
306
|
+
size: { width: values[0], height: values[1] },
|
|
307
|
+
cursor_position: { x: values[2], y: values[3] },
|
|
308
|
+
attributes: values[4],
|
|
309
|
+
window: {
|
|
310
|
+
left: values[5],
|
|
311
|
+
top: values[6],
|
|
312
|
+
right: values[7],
|
|
313
|
+
bottom: values[8]
|
|
314
|
+
},
|
|
315
|
+
max_window_size: { width: values[9], height: values[10] }
|
|
316
|
+
}
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Get the console window dimensions
|
|
320
|
+
# @return [Array<Integer>, nil] [width, height] or nil on failure
|
|
321
|
+
def get_size
|
|
322
|
+
return nil unless windows?
|
|
323
|
+
|
|
324
|
+
info = get_screen_buffer_info
|
|
325
|
+
return nil unless info
|
|
326
|
+
|
|
327
|
+
width = info[:window][:right] - info[:window][:left] + 1
|
|
328
|
+
height = info[:window][:bottom] - info[:window][:top] + 1
|
|
329
|
+
|
|
330
|
+
[width, height]
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Get current cursor position
|
|
334
|
+
# @return [Array<Integer>, nil] [x, y] or nil on failure
|
|
335
|
+
def get_cursor_position
|
|
336
|
+
return nil unless windows?
|
|
337
|
+
|
|
338
|
+
info = get_screen_buffer_info
|
|
339
|
+
return nil unless info
|
|
340
|
+
|
|
341
|
+
[info[:cursor_position][:x], info[:cursor_position][:y]]
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Set cursor position
|
|
345
|
+
# @param x [Integer] Column (0-indexed)
|
|
346
|
+
# @param y [Integer] Row (0-indexed)
|
|
347
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
348
|
+
# @return [Boolean] Success status
|
|
349
|
+
def set_cursor_position(x, y, handle = stdout_handle)
|
|
350
|
+
return false unless windows? && handle
|
|
351
|
+
|
|
352
|
+
# Pack COORD structure as DWORD (low word = X, high word = Y); mask
|
|
353
|
+
# both fields so a stray high bit can't corrupt the other coordinate.
|
|
354
|
+
coord = ((y & 0xFFFF) << 16) | (x & 0xFFFF)
|
|
355
|
+
SetConsoleCursorPosition(handle, coord) != 0
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Set console text attributes (foreground/background colors)
|
|
359
|
+
# @param attributes [Integer] Attribute flags
|
|
360
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
361
|
+
# @return [Boolean] Success status
|
|
362
|
+
def set_text_attribute(attributes, handle = stdout_handle)
|
|
363
|
+
return false unless windows? && handle
|
|
364
|
+
|
|
365
|
+
SetConsoleTextAttribute(handle, attributes) != 0
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Get current text attributes
|
|
369
|
+
# @return [Integer, nil] Current attributes or nil on failure
|
|
370
|
+
def get_text_attributes
|
|
371
|
+
return nil unless windows?
|
|
372
|
+
|
|
373
|
+
info = get_screen_buffer_info
|
|
374
|
+
return nil unless info
|
|
375
|
+
|
|
376
|
+
info[:attributes]
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Fill console output with a character
|
|
380
|
+
# @param char [String] Character to fill with
|
|
381
|
+
# @param length [Integer] Number of cells to fill
|
|
382
|
+
# @param x [Integer] Starting column
|
|
383
|
+
# @param y [Integer] Starting row
|
|
384
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
385
|
+
# @return [Integer, nil] Number of characters written or nil on failure
|
|
386
|
+
def fill_output_character(char, length, x, y, handle = stdout_handle)
|
|
387
|
+
return nil unless windows? && handle
|
|
388
|
+
|
|
389
|
+
# COORD is two SHORTs marshalled as a packed DWORD; mask both fields.
|
|
390
|
+
coord = ((y & 0xFFFF) << 16) | (x & 0xFFFF)
|
|
391
|
+
written_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_LONG, Fiddle::RUBY_FREE)
|
|
392
|
+
|
|
393
|
+
# cCharacter is a single UTF-16 code unit. Characters above U+FFFF
|
|
394
|
+
# cannot be represented; mask to the low 16 bits to make the truncation
|
|
395
|
+
# explicit rather than relying on Fiddle's silent narrowing.
|
|
396
|
+
char_code = char.ord & 0xFFFF
|
|
397
|
+
result = FillConsoleOutputCharacterW(handle, char_code, length, coord, written_ptr)
|
|
398
|
+
return nil if result == 0
|
|
399
|
+
|
|
400
|
+
written_ptr[0, Fiddle::SIZEOF_LONG].unpack1("L")
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Fill console output with an attribute
|
|
404
|
+
# @param attribute [Integer] Attribute to fill with
|
|
405
|
+
# @param length [Integer] Number of cells to fill
|
|
406
|
+
# @param x [Integer] Starting column
|
|
407
|
+
# @param y [Integer] Starting row
|
|
408
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
409
|
+
# @return [Integer, nil] Number of cells written or nil on failure
|
|
410
|
+
def fill_output_attribute(attribute, length, x, y, handle = stdout_handle)
|
|
411
|
+
return nil unless windows? && handle
|
|
412
|
+
|
|
413
|
+
coord = ((y & 0xFFFF) << 16) | (x & 0xFFFF)
|
|
414
|
+
written_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_LONG, Fiddle::RUBY_FREE)
|
|
415
|
+
|
|
416
|
+
result = FillConsoleOutputAttribute(handle, attribute, length, coord, written_ptr)
|
|
417
|
+
return nil if result == 0
|
|
418
|
+
|
|
419
|
+
written_ptr[0, Fiddle::SIZEOF_LONG].unpack1("L")
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# Set console window title
|
|
423
|
+
# @param title [String] New window title
|
|
424
|
+
# @return [Boolean] Success status
|
|
425
|
+
def set_title(title)
|
|
426
|
+
return false unless windows?
|
|
427
|
+
|
|
428
|
+
# Convert to UTF-16LE with null terminator
|
|
429
|
+
wide_title = (title + "\0").encode("UTF-16LE")
|
|
430
|
+
SetConsoleTitleW(Fiddle::Pointer[wide_title]) != 0
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# Show the cursor
|
|
434
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
435
|
+
# @return [Boolean] Success status
|
|
436
|
+
def show_cursor(handle = stdout_handle)
|
|
437
|
+
set_cursor_visibility(true, handle)
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# Hide the cursor
|
|
441
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
442
|
+
# @return [Boolean] Success status
|
|
443
|
+
def hide_cursor(handle = stdout_handle)
|
|
444
|
+
set_cursor_visibility(false, handle)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# Set cursor visibility
|
|
448
|
+
# @param visible [Boolean] Whether cursor should be visible
|
|
449
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
450
|
+
# @return [Boolean] Success status
|
|
451
|
+
def set_cursor_visibility(visible, handle = stdout_handle)
|
|
452
|
+
return false unless windows? && handle
|
|
453
|
+
|
|
454
|
+
# Get current cursor info
|
|
455
|
+
buffer = Fiddle::Pointer.malloc(CONSOLE_CURSOR_INFO_SIZE, Fiddle::RUBY_FREE)
|
|
456
|
+
result = GetConsoleCursorInfo(handle, buffer)
|
|
457
|
+
return false if result == 0
|
|
458
|
+
|
|
459
|
+
# Modify visibility
|
|
460
|
+
data = buffer[0, CONSOLE_CURSOR_INFO_SIZE].unpack("L L")
|
|
461
|
+
cursor_size = data[0]
|
|
462
|
+
buffer[0, CONSOLE_CURSOR_INFO_SIZE] = [cursor_size, visible ? 1 : 0].pack("L L")
|
|
463
|
+
|
|
464
|
+
SetConsoleCursorInfo(handle, buffer) != 0
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
# Write text to console (bypassing Ruby's IO buffering)
|
|
468
|
+
# @param text [String] Text to write
|
|
469
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
470
|
+
# @return [Integer, nil] Number of characters written or nil on failure
|
|
471
|
+
def write_console(text, handle = stdout_handle)
|
|
472
|
+
return nil unless windows? && handle
|
|
473
|
+
|
|
474
|
+
wide_text = text.encode("UTF-16LE")
|
|
475
|
+
# WriteConsoleW counts UTF-16 code UNITS, not Ruby characters (code
|
|
476
|
+
# points). Characters outside the BMP (e.g. emoji) encode as 2 units, so
|
|
477
|
+
# using text.length would truncate the tail. Derive the count from the
|
|
478
|
+
# encoded buffer.
|
|
479
|
+
char_count = wide_text.bytesize / 2
|
|
480
|
+
written_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_LONG, Fiddle::RUBY_FREE)
|
|
481
|
+
|
|
482
|
+
result = WriteConsoleW(handle, Fiddle::Pointer[wide_text], char_count, written_ptr, nil)
|
|
483
|
+
return nil if result == 0
|
|
484
|
+
|
|
485
|
+
written_ptr[0, Fiddle::SIZEOF_LONG].unpack1("L")
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# Clear the entire screen
|
|
489
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
490
|
+
# @return [Boolean] Success status
|
|
491
|
+
def clear_screen(handle = stdout_handle)
|
|
492
|
+
return false unless windows? && handle
|
|
493
|
+
|
|
494
|
+
info = get_screen_buffer_info(handle)
|
|
495
|
+
return false unless info
|
|
496
|
+
|
|
497
|
+
size = info[:size][:width] * info[:size][:height]
|
|
498
|
+
attributes = info[:attributes]
|
|
499
|
+
|
|
500
|
+
fill_output_character(" ", size, 0, 0, handle)
|
|
501
|
+
fill_output_attribute(attributes, size, 0, 0, handle)
|
|
502
|
+
set_cursor_position(0, 0, handle)
|
|
503
|
+
|
|
504
|
+
true
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# Erase from cursor to end of line
|
|
508
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
509
|
+
# @return [Boolean] Success status
|
|
510
|
+
def erase_line(handle = stdout_handle)
|
|
511
|
+
return false unless windows? && handle
|
|
512
|
+
|
|
513
|
+
info = get_screen_buffer_info(handle)
|
|
514
|
+
return false unless info
|
|
515
|
+
|
|
516
|
+
x = info[:cursor_position][:x]
|
|
517
|
+
y = info[:cursor_position][:y]
|
|
518
|
+
length = info[:size][:width] - x
|
|
519
|
+
attributes = info[:attributes]
|
|
520
|
+
|
|
521
|
+
fill_output_character(" ", length, x, y, handle)
|
|
522
|
+
fill_output_attribute(attributes, length, x, y, handle)
|
|
523
|
+
|
|
524
|
+
true
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
# Move cursor up
|
|
528
|
+
# @param lines [Integer] Number of lines to move up
|
|
529
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
530
|
+
# @return [Boolean] Success status
|
|
531
|
+
def cursor_up(lines = 1, handle = stdout_handle)
|
|
532
|
+
return false unless windows?
|
|
533
|
+
|
|
534
|
+
pos = get_cursor_position
|
|
535
|
+
return false unless pos
|
|
536
|
+
|
|
537
|
+
new_y = [pos[1] - lines, 0].max
|
|
538
|
+
set_cursor_position(pos[0], new_y, handle)
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
# Move cursor down
|
|
542
|
+
# @param lines [Integer] Number of lines to move down
|
|
543
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
544
|
+
# @return [Boolean] Success status
|
|
545
|
+
def cursor_down(lines = 1, handle = stdout_handle)
|
|
546
|
+
return false unless windows?
|
|
547
|
+
|
|
548
|
+
info = get_screen_buffer_info(handle)
|
|
549
|
+
return false unless info
|
|
550
|
+
|
|
551
|
+
pos = get_cursor_position
|
|
552
|
+
return false unless pos
|
|
553
|
+
|
|
554
|
+
max_y = info[:size][:height] - 1
|
|
555
|
+
new_y = [pos[1] + lines, max_y].min
|
|
556
|
+
set_cursor_position(pos[0], new_y, handle)
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
# Move cursor forward (right)
|
|
560
|
+
# @param columns [Integer] Number of columns to move
|
|
561
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
562
|
+
# @return [Boolean] Success status
|
|
563
|
+
def cursor_forward(columns = 1, handle = stdout_handle)
|
|
564
|
+
return false unless windows?
|
|
565
|
+
|
|
566
|
+
info = get_screen_buffer_info(handle)
|
|
567
|
+
return false unless info
|
|
568
|
+
|
|
569
|
+
pos = get_cursor_position
|
|
570
|
+
return false unless pos
|
|
571
|
+
|
|
572
|
+
max_x = info[:size][:width] - 1
|
|
573
|
+
new_x = [pos[0] + columns, max_x].min
|
|
574
|
+
set_cursor_position(new_x, pos[1], handle)
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
# Move cursor backward (left)
|
|
578
|
+
# @param columns [Integer] Number of columns to move
|
|
579
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
580
|
+
# @return [Boolean] Success status
|
|
581
|
+
def cursor_backward(columns = 1, handle = stdout_handle)
|
|
582
|
+
return false unless windows?
|
|
583
|
+
|
|
584
|
+
pos = get_cursor_position
|
|
585
|
+
return false unless pos
|
|
586
|
+
|
|
587
|
+
new_x = [pos[0] - columns, 0].max
|
|
588
|
+
set_cursor_position(new_x, pos[1], handle)
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
# Move cursor to the beginning of the line
|
|
592
|
+
# @param handle [Integer] Console handle (defaults to stdout)
|
|
593
|
+
# @return [Boolean] Success status
|
|
594
|
+
def cursor_to_column(column = 0, handle = stdout_handle)
|
|
595
|
+
return false unless windows?
|
|
596
|
+
|
|
597
|
+
pos = get_cursor_position
|
|
598
|
+
return false unless pos
|
|
599
|
+
|
|
600
|
+
set_cursor_position(column, pos[1], handle)
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
# Convert ANSI color number to Windows console attributes
|
|
604
|
+
# @param foreground [Integer, nil] ANSI foreground color (0-15)
|
|
605
|
+
# @param background [Integer, nil] ANSI background color (0-15)
|
|
606
|
+
# @return [Integer] Windows console attribute value
|
|
607
|
+
def ansi_to_windows_attributes(foreground: nil, background: nil)
|
|
608
|
+
attributes = 0
|
|
609
|
+
attributes |= ANSI_TO_WINDOWS_FG[foreground] if foreground && foreground < 16
|
|
610
|
+
attributes |= ANSI_TO_WINDOWS_BG[background] if background && background < 16
|
|
611
|
+
attributes
|
|
612
|
+
end
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
# NOTE: ANSI is enabled lazily by Console#initialize (and by the global
|
|
616
|
+
# Rich console on first use), NOT at require time. Mutating the real
|
|
617
|
+
# console's VT mode merely because the gem was loaded is a surprising,
|
|
618
|
+
# unrestored global side effect, so it is intentionally not done here.
|
|
619
|
+
end
|
|
620
|
+
end
|