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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +546 -0
- data/examples/demo.rb +106 -0
- data/examples/showcase.rb +420 -0
- data/examples/smoke_test.rb +41 -0
- data/examples/stress_test.rb +604 -0
- data/examples/syntax_markdown_demo.rb +166 -0
- data/examples/verify.rb +215 -0
- data/examples/visual_demo.rb +145 -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 +582 -0
- data/lib/rich.rb +108 -0
- metadata +106 -0
|
@@ -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"
|