rich-ruby 1.0.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,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
+ end
143
+
144
+ # CONSOLE_SCREEN_BUFFER_INFO structure layout:
145
+ # typedef struct _CONSOLE_SCREEN_BUFFER_INFO {
146
+ # COORD dwSize; // 4 bytes (2x SHORT)
147
+ # COORD dwCursorPosition; // 4 bytes (2x SHORT)
148
+ # WORD wAttributes; // 2 bytes
149
+ # SMALL_RECT srWindow; // 8 bytes (4x SHORT)
150
+ # COORD dwMaximumWindowSize; // 4 bytes (2x SHORT)
151
+ # } CONSOLE_SCREEN_BUFFER_INFO;
152
+ # Total: 22 bytes
153
+ CONSOLE_SCREEN_BUFFER_INFO_SIZE = 22
154
+
155
+ # CONSOLE_CURSOR_INFO structure layout:
156
+ # typedef struct _CONSOLE_CURSOR_INFO {
157
+ # DWORD dwSize; // 4 bytes
158
+ # BOOL bVisible; // 4 bytes
159
+ # } CONSOLE_CURSOR_INFO;
160
+ # Total: 8 bytes
161
+ CONSOLE_CURSOR_INFO_SIZE = 8
162
+
163
+ class << self
164
+ # @return [Boolean] Whether the current platform is Windows
165
+ def windows?
166
+ Gem.win_platform?
167
+ end
168
+
169
+ # @return [Integer] Handle to stdout
170
+ def stdout_handle
171
+ return nil unless windows?
172
+ @stdout_handle ||= GetStdHandle(STD_OUTPUT_HANDLE)
173
+ end
174
+
175
+ # @return [Integer] Handle to stdin
176
+ def stdin_handle
177
+ return nil unless windows?
178
+ @stdin_handle ||= GetStdHandle(STD_INPUT_HANDLE)
179
+ end
180
+
181
+ # @return [Integer] Handle to stderr
182
+ def stderr_handle
183
+ return nil unless windows?
184
+ @stderr_handle ||= GetStdHandle(STD_ERROR_HANDLE)
185
+ end
186
+
187
+ # Get the current console mode for a handle
188
+ # @param handle [Integer] Console handle (defaults to stdout)
189
+ # @return [Integer, nil] Console mode flags or nil on failure
190
+ def get_console_mode(handle = stdout_handle)
191
+ return nil unless windows? && handle
192
+
193
+ mode_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_LONG, Fiddle::RUBY_FREE)
194
+ result = GetConsoleMode(handle, mode_ptr)
195
+ return nil if result == 0
196
+
197
+ mode_ptr[0, Fiddle::SIZEOF_LONG].unpack1("L")
198
+ end
199
+
200
+ # Set the console mode for a handle
201
+ # @param mode [Integer] Console mode flags
202
+ # @param handle [Integer] Console handle (defaults to stdout)
203
+ # @return [Boolean] Success status
204
+ def set_console_mode(mode, handle = stdout_handle)
205
+ return false unless windows? && handle
206
+
207
+ SetConsoleMode(handle, mode) != 0
208
+ end
209
+
210
+ # Check if virtual terminal (ANSI) processing is supported
211
+ # @return [Boolean] True if ANSI escape sequences are supported
212
+ def supports_ansi?
213
+ return @supports_ansi if defined?(@supports_ansi)
214
+
215
+ unless windows?
216
+ @supports_ansi = true # Unix terminals support ANSI
217
+ return @supports_ansi
218
+ end
219
+
220
+ mode = get_console_mode
221
+ return @supports_ansi = false if mode.nil?
222
+
223
+ @supports_ansi = (mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0
224
+ end
225
+
226
+ # Enable virtual terminal (ANSI) processing
227
+ # @return [Boolean] True if ANSI mode was successfully enabled
228
+ def enable_ansi!
229
+ return true unless windows? # Already supported on Unix
230
+
231
+ handle = stdout_handle
232
+ return false unless handle
233
+
234
+ current_mode = get_console_mode(handle)
235
+ return false unless current_mode
236
+
237
+ new_mode = current_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING
238
+ result = set_console_mode(new_mode, handle)
239
+
240
+ # Update cached value
241
+ @supports_ansi = result
242
+ result
243
+ end
244
+
245
+ # Disable virtual terminal (ANSI) processing
246
+ # @return [Boolean] True if ANSI mode was successfully disabled
247
+ def disable_ansi!
248
+ return false unless windows?
249
+
250
+ handle = stdout_handle
251
+ return false unless handle
252
+
253
+ current_mode = get_console_mode(handle)
254
+ return false unless current_mode
255
+
256
+ new_mode = current_mode & ~ENABLE_VIRTUAL_TERMINAL_PROCESSING
257
+ result = set_console_mode(new_mode, handle)
258
+
259
+ @supports_ansi = !result if result
260
+ result
261
+ end
262
+
263
+ # Get console screen buffer info
264
+ # @param handle [Integer] Console handle (defaults to stdout)
265
+ # @return [Hash, nil] Screen buffer info or nil on failure
266
+ def get_screen_buffer_info(handle = stdout_handle)
267
+ return nil unless windows? && handle
268
+
269
+ buffer = Fiddle::Pointer.malloc(CONSOLE_SCREEN_BUFFER_INFO_SIZE, Fiddle::RUBY_FREE)
270
+ result = GetConsoleScreenBufferInfo(handle, buffer)
271
+ return nil if result == 0
272
+
273
+ data = buffer[0, CONSOLE_SCREEN_BUFFER_INFO_SIZE]
274
+
275
+ # Unpack the structure
276
+ values = data.unpack("s2 s2 S s4 s2")
277
+
278
+ {
279
+ size: { width: values[0], height: values[1] },
280
+ cursor_position: { x: values[2], y: values[3] },
281
+ attributes: values[4],
282
+ window: {
283
+ left: values[5],
284
+ top: values[6],
285
+ right: values[7],
286
+ bottom: values[8]
287
+ },
288
+ max_window_size: { width: values[9], height: values[10] }
289
+ }
290
+ end
291
+
292
+ # Get the console window dimensions
293
+ # @return [Array<Integer>, nil] [width, height] or nil on failure
294
+ def get_size
295
+ return nil unless windows?
296
+
297
+ info = get_screen_buffer_info
298
+ return nil unless info
299
+
300
+ width = info[:window][:right] - info[:window][:left] + 1
301
+ height = info[:window][:bottom] - info[:window][:top] + 1
302
+
303
+ [width, height]
304
+ end
305
+
306
+ # Get current cursor position
307
+ # @return [Array<Integer>, nil] [x, y] or nil on failure
308
+ def get_cursor_position
309
+ return nil unless windows?
310
+
311
+ info = get_screen_buffer_info
312
+ return nil unless info
313
+
314
+ [info[:cursor_position][:x], info[:cursor_position][:y]]
315
+ end
316
+
317
+ # Set cursor position
318
+ # @param x [Integer] Column (0-indexed)
319
+ # @param y [Integer] Row (0-indexed)
320
+ # @param handle [Integer] Console handle (defaults to stdout)
321
+ # @return [Boolean] Success status
322
+ def set_cursor_position(x, y, handle = stdout_handle)
323
+ return false unless windows? && handle
324
+
325
+ # Pack COORD structure as DWORD (low word = X, high word = Y)
326
+ coord = (y << 16) | (x & 0xFFFF)
327
+ SetConsoleCursorPosition(handle, coord) != 0
328
+ end
329
+
330
+ # Set console text attributes (foreground/background colors)
331
+ # @param attributes [Integer] Attribute flags
332
+ # @param handle [Integer] Console handle (defaults to stdout)
333
+ # @return [Boolean] Success status
334
+ def set_text_attribute(attributes, handle = stdout_handle)
335
+ return false unless windows? && handle
336
+
337
+ SetConsoleTextAttribute(handle, attributes) != 0
338
+ end
339
+
340
+ # Get current text attributes
341
+ # @return [Integer, nil] Current attributes or nil on failure
342
+ def get_text_attributes
343
+ return nil unless windows?
344
+
345
+ info = get_screen_buffer_info
346
+ return nil unless info
347
+
348
+ info[:attributes]
349
+ end
350
+
351
+ # Fill console output with a character
352
+ # @param char [String] Character to fill with
353
+ # @param length [Integer] Number of cells to fill
354
+ # @param x [Integer] Starting column
355
+ # @param y [Integer] Starting row
356
+ # @param handle [Integer] Console handle (defaults to stdout)
357
+ # @return [Integer, nil] Number of characters written or nil on failure
358
+ def fill_output_character(char, length, x, y, handle = stdout_handle)
359
+ return nil unless windows? && handle
360
+
361
+ coord = (y << 16) | (x & 0xFFFF)
362
+ written_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_LONG, Fiddle::RUBY_FREE)
363
+
364
+ char_code = char.ord
365
+ result = FillConsoleOutputCharacterW(handle, char_code, length, coord, written_ptr)
366
+ return nil if result == 0
367
+
368
+ written_ptr[0, Fiddle::SIZEOF_LONG].unpack1("L")
369
+ end
370
+
371
+ # Fill console output with an attribute
372
+ # @param attribute [Integer] Attribute to fill with
373
+ # @param length [Integer] Number of cells to fill
374
+ # @param x [Integer] Starting column
375
+ # @param y [Integer] Starting row
376
+ # @param handle [Integer] Console handle (defaults to stdout)
377
+ # @return [Integer, nil] Number of cells written or nil on failure
378
+ def fill_output_attribute(attribute, length, x, y, handle = stdout_handle)
379
+ return nil unless windows? && handle
380
+
381
+ coord = (y << 16) | (x & 0xFFFF)
382
+ written_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_LONG, Fiddle::RUBY_FREE)
383
+
384
+ result = FillConsoleOutputAttribute(handle, attribute, length, coord, written_ptr)
385
+ return nil if result == 0
386
+
387
+ written_ptr[0, Fiddle::SIZEOF_LONG].unpack1("L")
388
+ end
389
+
390
+ # Set console window title
391
+ # @param title [String] New window title
392
+ # @return [Boolean] Success status
393
+ def set_title(title)
394
+ return false unless windows?
395
+
396
+ # Convert to UTF-16LE with null terminator
397
+ wide_title = (title + "\0").encode("UTF-16LE")
398
+ SetConsoleTitleW(Fiddle::Pointer[wide_title]) != 0
399
+ end
400
+
401
+ # Show the cursor
402
+ # @param handle [Integer] Console handle (defaults to stdout)
403
+ # @return [Boolean] Success status
404
+ def show_cursor(handle = stdout_handle)
405
+ set_cursor_visibility(true, handle)
406
+ end
407
+
408
+ # Hide the cursor
409
+ # @param handle [Integer] Console handle (defaults to stdout)
410
+ # @return [Boolean] Success status
411
+ def hide_cursor(handle = stdout_handle)
412
+ set_cursor_visibility(false, handle)
413
+ end
414
+
415
+ # Set cursor visibility
416
+ # @param visible [Boolean] Whether cursor should be visible
417
+ # @param handle [Integer] Console handle (defaults to stdout)
418
+ # @return [Boolean] Success status
419
+ def set_cursor_visibility(visible, handle = stdout_handle)
420
+ return false unless windows? && handle
421
+
422
+ # Get current cursor info
423
+ buffer = Fiddle::Pointer.malloc(CONSOLE_CURSOR_INFO_SIZE, Fiddle::RUBY_FREE)
424
+ result = GetConsoleCursorInfo(handle, buffer)
425
+ return false if result == 0
426
+
427
+ # Modify visibility
428
+ data = buffer[0, CONSOLE_CURSOR_INFO_SIZE].unpack("L L")
429
+ cursor_size = data[0]
430
+ buffer[0, CONSOLE_CURSOR_INFO_SIZE] = [cursor_size, visible ? 1 : 0].pack("L L")
431
+
432
+ SetConsoleCursorInfo(handle, buffer) != 0
433
+ end
434
+
435
+ # Write text to console (bypassing Ruby's IO buffering)
436
+ # @param text [String] Text to write
437
+ # @param handle [Integer] Console handle (defaults to stdout)
438
+ # @return [Integer, nil] Number of characters written or nil on failure
439
+ def write_console(text, handle = stdout_handle)
440
+ return nil unless windows? && handle
441
+
442
+ wide_text = text.encode("UTF-16LE")
443
+ char_count = text.length
444
+ written_ptr = Fiddle::Pointer.malloc(Fiddle::SIZEOF_LONG, Fiddle::RUBY_FREE)
445
+
446
+ result = WriteConsoleW(handle, Fiddle::Pointer[wide_text], char_count, written_ptr, nil)
447
+ return nil if result == 0
448
+
449
+ written_ptr[0, Fiddle::SIZEOF_LONG].unpack1("L")
450
+ end
451
+
452
+ # Clear the entire screen
453
+ # @param handle [Integer] Console handle (defaults to stdout)
454
+ # @return [Boolean] Success status
455
+ def clear_screen(handle = stdout_handle)
456
+ return false unless windows? && handle
457
+
458
+ info = get_screen_buffer_info(handle)
459
+ return false unless info
460
+
461
+ size = info[:size][:width] * info[:size][:height]
462
+ attributes = info[:attributes]
463
+
464
+ fill_output_character(" ", size, 0, 0, handle)
465
+ fill_output_attribute(attributes, size, 0, 0, handle)
466
+ set_cursor_position(0, 0, handle)
467
+
468
+ true
469
+ end
470
+
471
+ # Erase from cursor to end of line
472
+ # @param handle [Integer] Console handle (defaults to stdout)
473
+ # @return [Boolean] Success status
474
+ def erase_line(handle = stdout_handle)
475
+ return false unless windows? && handle
476
+
477
+ info = get_screen_buffer_info(handle)
478
+ return false unless info
479
+
480
+ x = info[:cursor_position][:x]
481
+ y = info[:cursor_position][:y]
482
+ length = info[:size][:width] - x
483
+ attributes = info[:attributes]
484
+
485
+ fill_output_character(" ", length, x, y, handle)
486
+ fill_output_attribute(attributes, length, x, y, handle)
487
+
488
+ true
489
+ end
490
+
491
+ # Move cursor up
492
+ # @param lines [Integer] Number of lines to move up
493
+ # @param handle [Integer] Console handle (defaults to stdout)
494
+ # @return [Boolean] Success status
495
+ def cursor_up(lines = 1, handle = stdout_handle)
496
+ return false unless windows?
497
+
498
+ pos = get_cursor_position
499
+ return false unless pos
500
+
501
+ new_y = [pos[1] - lines, 0].max
502
+ set_cursor_position(pos[0], new_y, handle)
503
+ end
504
+
505
+ # Move cursor down
506
+ # @param lines [Integer] Number of lines to move down
507
+ # @param handle [Integer] Console handle (defaults to stdout)
508
+ # @return [Boolean] Success status
509
+ def cursor_down(lines = 1, handle = stdout_handle)
510
+ return false unless windows?
511
+
512
+ info = get_screen_buffer_info(handle)
513
+ return false unless info
514
+
515
+ pos = get_cursor_position
516
+ return false unless pos
517
+
518
+ max_y = info[:size][:height] - 1
519
+ new_y = [pos[1] + lines, max_y].min
520
+ set_cursor_position(pos[0], new_y, handle)
521
+ end
522
+
523
+ # Move cursor forward (right)
524
+ # @param columns [Integer] Number of columns to move
525
+ # @param handle [Integer] Console handle (defaults to stdout)
526
+ # @return [Boolean] Success status
527
+ def cursor_forward(columns = 1, handle = stdout_handle)
528
+ return false unless windows?
529
+
530
+ info = get_screen_buffer_info(handle)
531
+ return false unless info
532
+
533
+ pos = get_cursor_position
534
+ return false unless pos
535
+
536
+ max_x = info[:size][:width] - 1
537
+ new_x = [pos[0] + columns, max_x].min
538
+ set_cursor_position(new_x, pos[1], handle)
539
+ end
540
+
541
+ # Move cursor backward (left)
542
+ # @param columns [Integer] Number of columns to move
543
+ # @param handle [Integer] Console handle (defaults to stdout)
544
+ # @return [Boolean] Success status
545
+ def cursor_backward(columns = 1, handle = stdout_handle)
546
+ return false unless windows?
547
+
548
+ pos = get_cursor_position
549
+ return false unless pos
550
+
551
+ new_x = [pos[0] - columns, 0].max
552
+ set_cursor_position(new_x, pos[1], handle)
553
+ end
554
+
555
+ # Move cursor to the beginning of the line
556
+ # @param handle [Integer] Console handle (defaults to stdout)
557
+ # @return [Boolean] Success status
558
+ def cursor_to_column(column = 0, handle = stdout_handle)
559
+ return false unless windows?
560
+
561
+ pos = get_cursor_position
562
+ return false unless pos
563
+
564
+ set_cursor_position(column, pos[1], handle)
565
+ end
566
+
567
+ # Convert ANSI color number to Windows console attributes
568
+ # @param foreground [Integer, nil] ANSI foreground color (0-15)
569
+ # @param background [Integer, nil] ANSI background color (0-15)
570
+ # @return [Integer] Windows console attribute value
571
+ def ansi_to_windows_attributes(foreground: nil, background: nil)
572
+ attributes = 0
573
+ attributes |= ANSI_TO_WINDOWS_FG[foreground] if foreground && foreground < 16
574
+ attributes |= ANSI_TO_WINDOWS_BG[background] if background && background < 16
575
+ attributes
576
+ end
577
+ end
578
+
579
+ # Auto-enable ANSI on Windows when this module is loaded
580
+ enable_ansi! if windows?
581
+ end
582
+ end
data/lib/rich.rb ADDED
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Rich - A Ruby library for rich text and beautiful formatting in the terminal.
4
+ #
5
+ # Rich provides a simple API for adding color and style to terminal output.
6
+ # It supports true color (24-bit), 256-color, and 16-color terminals with
7
+ # automatic fallback. Full Windows Console API support included.
8
+ #
9
+ # @example Basic usage
10
+ # require 'rich'
11
+ #
12
+ # Rich.print("[bold red]Hello[/] [green]World[/]!")
13
+ # Rich.print("[italic blue on white]Styled text[/]")
14
+ #
15
+ # @example Using the Console directly
16
+ # console = Rich::Console.new
17
+ # console.print("Hello", style: "bold magenta")
18
+ #
19
+ # @example Tables
20
+ # table = Rich::Table.new(title: "Users")
21
+ # table.add_column("Name")
22
+ # table.add_column("Age")
23
+ # table.add_row("Alice", "30")
24
+ # Rich.print(table)
25
+
26
+ require_relative "rich/version"
27
+ require_relative "rich/color_triplet"
28
+ require_relative "rich/_palettes"
29
+ require_relative "rich/color"
30
+ require_relative "rich/terminal_theme"
31
+ require_relative "rich/style"
32
+ require_relative "rich/cells"
33
+ require_relative "rich/control"
34
+ require_relative "rich/segment"
35
+ require_relative "rich/text"
36
+ require_relative "rich/markup"
37
+ require_relative "rich/box"
38
+ require_relative "rich/panel"
39
+ require_relative "rich/table"
40
+ require_relative "rich/progress"
41
+ require_relative "rich/tree"
42
+ require_relative "rich/json"
43
+ require_relative "rich/layout"
44
+ require_relative "rich/syntax"
45
+ require_relative "rich/markdown"
46
+ require_relative "rich/win32_console" if Gem.win_platform?
47
+
48
+ module Rich
49
+ class << self
50
+ # Global console instance
51
+ # @return [Console, nil]
52
+ attr_accessor :console
53
+
54
+ # Get or create the global console
55
+ # @return [Console]
56
+ def get_console
57
+ @console ||= Console.new
58
+ end
59
+
60
+ # Reconfigure the global console
61
+ # @param kwargs [Hash] Console options
62
+ # @return [void]
63
+ def reconfigure(**kwargs)
64
+ @console = Console.new(**kwargs)
65
+ end
66
+
67
+ # Print to the console with markup support
68
+ # @param objects [Array] Objects to print
69
+ # @param sep [String] Separator between objects
70
+ # @param end_str [String] End of line string
71
+ # @param style [String, Style, nil] Style to apply
72
+ # @param highlight [Boolean] Enable highlighting
73
+ # @return [void]
74
+ def print(*objects, sep: " ", end_str: "\n", style: nil, highlight: true)
75
+ get_console.print(*objects, sep: sep, end_str: end_str, style: style, highlight: highlight)
76
+ end
77
+
78
+ # Print JSON with syntax highlighting
79
+ # @param json [String, nil] JSON string
80
+ # @param data [Object] Object to convert to JSON
81
+ # @param indent [Integer] Indentation level
82
+ # @return [void]
83
+ def print_json(json = nil, data: nil, indent: 2)
84
+ get_console.print_json(json, data: data, indent: indent)
85
+ end
86
+
87
+ # Inspect an object and print its details
88
+ # @param obj [Object] Object to inspect
89
+ # @param title [String, nil] Title
90
+ # @param methods [Boolean] Show methods
91
+ # @param docs [Boolean] Show documentation
92
+ # @return [void]
93
+ def inspect(obj, title: nil, methods: false, docs: true)
94
+ get_console.inspect(obj, title: title, methods: methods, docs: docs)
95
+ end
96
+
97
+ # Create a rule/separator line
98
+ # @param title [String] Title text
99
+ # @param style [String] Style for the rule
100
+ # @return [void]
101
+ def rule(title = "", style: "rule.line")
102
+ get_console.rule(title, style: style)
103
+ end
104
+ end
105
+ end
106
+
107
+ # Auto-require Console after base modules are loaded
108
+ require_relative "rich/console"