ruby_rich 0.4.0 → 0.4.1
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/lib/ruby_rich/agent_shell.rb +254 -0
- data/lib/ruby_rich/ansi_code.rb +46 -0
- data/lib/ruby_rich/app_shell.rb +374 -0
- data/lib/ruby_rich/attachment.rb +25 -0
- data/lib/ruby_rich/composer.rb +512 -0
- data/lib/ruby_rich/console.rb +174 -25
- data/lib/ruby_rich/dialog.rb +2 -1
- data/lib/ruby_rich/event.rb +29 -0
- data/lib/ruby_rich/focus_manager.rb +77 -0
- data/lib/ruby_rich/layout.rb +117 -29
- data/lib/ruby_rich/line_editor.rb +325 -0
- data/lib/ruby_rich/live.rb +100 -19
- data/lib/ruby_rich/markdown.rb +100 -230
- data/lib/ruby_rich/panel.rb +1 -1
- data/lib/ruby_rich/print.rb +6 -6
- data/lib/ruby_rich/progress_manager.rb +150 -0
- data/lib/ruby_rich/sidebar.rb +85 -0
- data/lib/ruby_rich/slash_input.rb +197 -0
- data/lib/ruby_rich/table.rb +12 -12
- data/lib/ruby_rich/terminal.rb +510 -0
- data/lib/ruby_rich/text.rb +1 -1
- data/lib/ruby_rich/theme.rb +96 -0
- data/lib/ruby_rich/tool_block.rb +92 -0
- data/lib/ruby_rich/transcript.rb +553 -0
- data/lib/ruby_rich/version.rb +1 -1
- data/lib/ruby_rich/viewport.rb +468 -0
- data/lib/ruby_rich.rb +38 -13
- metadata +23 -22
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rbconfig'
|
|
4
|
+
require 'tty-cursor'
|
|
5
|
+
require_relative 'event'
|
|
6
|
+
|
|
7
|
+
module RubyRich
|
|
8
|
+
class Terminal
|
|
9
|
+
MOUSE_ENABLE = "\e[?1000h\e[?1002h\e[?1006h"
|
|
10
|
+
MOUSE_DISABLE = "\e[?1006l\e[?1002l\e[?1000l"
|
|
11
|
+
AUTOWRAP_ENABLE = "\e[?7h"
|
|
12
|
+
AUTOWRAP_DISABLE = "\e[?7l"
|
|
13
|
+
ALT_SCREEN_ENABLE = "\e[?1049h"
|
|
14
|
+
ALT_SCREEN_DISABLE = "\e[?1049l"
|
|
15
|
+
BRACKETED_PASTE_ENABLE = "\e[?2004h"
|
|
16
|
+
BRACKETED_PASTE_DISABLE = "\e[?2004l"
|
|
17
|
+
STD_INPUT_HANDLE = -10
|
|
18
|
+
STD_OUTPUT_HANDLE = -11
|
|
19
|
+
ENABLE_MOUSE_INPUT = 0x0010
|
|
20
|
+
ENABLE_WINDOW_INPUT = 0x0008
|
|
21
|
+
ENABLE_QUICK_EDIT_MODE = 0x0040
|
|
22
|
+
ENABLE_EXTENDED_FLAGS = 0x0080
|
|
23
|
+
ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
|
|
24
|
+
ENABLE_PROCESSED_OUTPUT = 0x0001
|
|
25
|
+
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
|
|
26
|
+
INPUT_RECORD_SIZE = 20
|
|
27
|
+
KEY_EVENT = 0x0001
|
|
28
|
+
MOUSE_EVENT = 0x0002
|
|
29
|
+
FROM_LEFT_1ST_BUTTON_PRESSED = 0x0001
|
|
30
|
+
RIGHTMOST_BUTTON_PRESSED = 0x0002
|
|
31
|
+
FROM_LEFT_2ND_BUTTON_PRESSED = 0x0004
|
|
32
|
+
MOUSE_MOVED = 0x0001
|
|
33
|
+
DOUBLE_CLICK = 0x0002
|
|
34
|
+
MOUSE_WHEELED = 0x0004
|
|
35
|
+
MOUSE_HWHEELED = 0x0008
|
|
36
|
+
RIGHT_ALT_PRESSED = 0x0001
|
|
37
|
+
LEFT_ALT_PRESSED = 0x0002
|
|
38
|
+
RIGHT_CTRL_PRESSED = 0x0004
|
|
39
|
+
LEFT_CTRL_PRESSED = 0x0008
|
|
40
|
+
SHIFT_PRESSED = 0x0010
|
|
41
|
+
|
|
42
|
+
class << self
|
|
43
|
+
attr_accessor :debug_input
|
|
44
|
+
|
|
45
|
+
def setup(mouse: false, hide_cursor: true, autowrap: true, alt_screen: false)
|
|
46
|
+
capture_state
|
|
47
|
+
enable_virtual_terminal_on_windows
|
|
48
|
+
system('stty -echo') unless windows?
|
|
49
|
+
enter_alt_screen if alt_screen
|
|
50
|
+
enable_bracketed_paste
|
|
51
|
+
set_autowrap(autowrap)
|
|
52
|
+
print TTY::Cursor.hide if hide_cursor
|
|
53
|
+
enable_mouse if mouse
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def restore(mouse: false, show_cursor: true, autowrap: true, alt_screen: false)
|
|
57
|
+
disable_mouse if mouse
|
|
58
|
+
disable_bracketed_paste
|
|
59
|
+
set_autowrap(autowrap)
|
|
60
|
+
leave_alt_screen if alt_screen
|
|
61
|
+
restore_virtual_terminal_on_windows
|
|
62
|
+
system("stty #{@original_state}") if @original_state && !windows?
|
|
63
|
+
print TTY::Cursor.show if show_cursor
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def enable_mouse
|
|
67
|
+
@mouse_reporting_enabled = true
|
|
68
|
+
enable_windows_input_mode if windows?
|
|
69
|
+
print MOUSE_ENABLE
|
|
70
|
+
$stdout.flush
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def disable_mouse
|
|
74
|
+
@mouse_reporting_enabled = false
|
|
75
|
+
print MOUSE_DISABLE
|
|
76
|
+
$stdout.flush
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def set_autowrap(enabled)
|
|
80
|
+
print(enabled ? AUTOWRAP_ENABLE : AUTOWRAP_DISABLE)
|
|
81
|
+
$stdout.flush
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def enter_alt_screen
|
|
85
|
+
print ALT_SCREEN_ENABLE
|
|
86
|
+
$stdout.flush
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def leave_alt_screen
|
|
90
|
+
print ALT_SCREEN_DISABLE
|
|
91
|
+
$stdout.flush
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def enable_bracketed_paste
|
|
95
|
+
print BRACKETED_PASTE_ENABLE
|
|
96
|
+
$stdout.flush
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def disable_bracketed_paste
|
|
100
|
+
print BRACKETED_PASTE_DISABLE
|
|
101
|
+
$stdout.flush
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def clear
|
|
105
|
+
print "\e[2J\e[H"
|
|
106
|
+
$stdout.flush
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def with_cooked(mouse: false, alt_screen: false)
|
|
110
|
+
restore(mouse: mouse, alt_screen: alt_screen)
|
|
111
|
+
yield
|
|
112
|
+
ensure
|
|
113
|
+
setup(mouse: mouse, alt_screen: alt_screen, autowrap: false)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def prepare_input
|
|
117
|
+
return unless windows?
|
|
118
|
+
|
|
119
|
+
enable_windows_input_mode
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def windows_input_mode
|
|
123
|
+
return nil unless windows?
|
|
124
|
+
|
|
125
|
+
console_mode(get_std_handle.call(STD_INPUT_HANDLE))
|
|
126
|
+
rescue
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def windows?
|
|
131
|
+
RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def windows_mouse_reporting?
|
|
135
|
+
windows? && @mouse_reporting_enabled
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def read_windows_input_event
|
|
139
|
+
return nil unless windows?
|
|
140
|
+
pending = next_windows_pending_event
|
|
141
|
+
return pending if pending
|
|
142
|
+
|
|
143
|
+
enable_windows_input_mode
|
|
144
|
+
handle = get_std_handle.call(STD_INPUT_HANDLE)
|
|
145
|
+
record = Fiddle::Pointer.malloc(INPUT_RECORD_SIZE)
|
|
146
|
+
read_count = Fiddle::Pointer.malloc(4)
|
|
147
|
+
|
|
148
|
+
loop do
|
|
149
|
+
return nil unless read_console_input.call(handle, record, 1, read_count) != 0
|
|
150
|
+
next unless read_count[0, 4].unpack1('L') == 1
|
|
151
|
+
|
|
152
|
+
event = parse_windows_input_record(record)
|
|
153
|
+
event = coalesce_windows_paste(handle, event) if paste_seed_event?(event)
|
|
154
|
+
return event if event
|
|
155
|
+
end
|
|
156
|
+
rescue
|
|
157
|
+
nil
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
def capture_state
|
|
163
|
+
@original_state = windows? ? nil : `stty -g`.strip
|
|
164
|
+
rescue
|
|
165
|
+
@original_state = nil
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def enable_virtual_terminal_on_windows
|
|
169
|
+
return unless windows?
|
|
170
|
+
|
|
171
|
+
@windows_console_modes ||= {}
|
|
172
|
+
|
|
173
|
+
enable_windows_output_mode
|
|
174
|
+
enable_windows_input_mode
|
|
175
|
+
rescue
|
|
176
|
+
nil
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def enable_windows_output_mode
|
|
180
|
+
handle = get_std_handle.call(STD_OUTPUT_HANDLE)
|
|
181
|
+
mode = console_mode(handle)
|
|
182
|
+
return unless mode
|
|
183
|
+
|
|
184
|
+
@windows_console_modes ||= {}
|
|
185
|
+
@windows_console_modes[handle.to_i] ||= { handle: handle, mode: mode }
|
|
186
|
+
set_console_mode.call(handle, mode | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def enable_windows_input_mode
|
|
190
|
+
handle = get_std_handle.call(STD_INPUT_HANDLE)
|
|
191
|
+
mode = console_mode(handle)
|
|
192
|
+
return unless mode
|
|
193
|
+
|
|
194
|
+
@windows_console_modes ||= {}
|
|
195
|
+
@windows_console_modes[handle.to_i] ||= { handle: handle, mode: mode }
|
|
196
|
+
next_mode = mode
|
|
197
|
+
next_mode |= ENABLE_EXTENDED_FLAGS
|
|
198
|
+
next_mode |= ENABLE_MOUSE_INPUT
|
|
199
|
+
next_mode |= ENABLE_WINDOW_INPUT
|
|
200
|
+
next_mode &= ~ENABLE_QUICK_EDIT_MODE
|
|
201
|
+
set_console_mode.call(handle, next_mode)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def restore_virtual_terminal_on_windows
|
|
205
|
+
return unless windows?
|
|
206
|
+
return unless @windows_console_modes && !@windows_console_modes.empty?
|
|
207
|
+
|
|
208
|
+
require 'fiddle'
|
|
209
|
+
kernel32 = Fiddle.dlopen('kernel32')
|
|
210
|
+
set_console_mode = Fiddle::Function.new(
|
|
211
|
+
kernel32['SetConsoleMode'],
|
|
212
|
+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_LONG],
|
|
213
|
+
Fiddle::TYPE_INT
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
@windows_console_modes.each_value do |entry|
|
|
217
|
+
set_console_mode.call(entry[:handle], entry[:mode])
|
|
218
|
+
end
|
|
219
|
+
@windows_console_modes.clear
|
|
220
|
+
rescue
|
|
221
|
+
nil
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def console_mode(handle)
|
|
225
|
+
mode_ptr = Fiddle::Pointer.malloc(4)
|
|
226
|
+
return nil unless get_console_mode.call(handle, mode_ptr) != 0
|
|
227
|
+
|
|
228
|
+
mode_ptr[0, 4].unpack1('L')
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def kernel32
|
|
232
|
+
@kernel32 ||= begin
|
|
233
|
+
ensure_fiddle
|
|
234
|
+
Fiddle.dlopen('kernel32')
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def get_std_handle
|
|
239
|
+
ensure_fiddle
|
|
240
|
+
@get_std_handle ||= Fiddle::Function.new(
|
|
241
|
+
kernel32['GetStdHandle'],
|
|
242
|
+
[Fiddle::TYPE_LONG],
|
|
243
|
+
Fiddle::TYPE_VOIDP
|
|
244
|
+
)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def get_console_mode
|
|
248
|
+
ensure_fiddle
|
|
249
|
+
@get_console_mode ||= Fiddle::Function.new(
|
|
250
|
+
kernel32['GetConsoleMode'],
|
|
251
|
+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP],
|
|
252
|
+
Fiddle::TYPE_INT
|
|
253
|
+
)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def set_console_mode
|
|
257
|
+
ensure_fiddle
|
|
258
|
+
@set_console_mode ||= Fiddle::Function.new(
|
|
259
|
+
kernel32['SetConsoleMode'],
|
|
260
|
+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_LONG],
|
|
261
|
+
Fiddle::TYPE_INT
|
|
262
|
+
)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def read_console_input
|
|
266
|
+
ensure_fiddle
|
|
267
|
+
@read_console_input ||= Fiddle::Function.new(
|
|
268
|
+
kernel32['ReadConsoleInputW'],
|
|
269
|
+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_LONG, Fiddle::TYPE_VOIDP],
|
|
270
|
+
Fiddle::TYPE_INT
|
|
271
|
+
)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def get_number_of_console_input_events
|
|
275
|
+
ensure_fiddle
|
|
276
|
+
@get_number_of_console_input_events ||= Fiddle::Function.new(
|
|
277
|
+
kernel32['GetNumberOfConsoleInputEvents'],
|
|
278
|
+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP],
|
|
279
|
+
Fiddle::TYPE_INT
|
|
280
|
+
)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def parse_windows_input_record(record)
|
|
284
|
+
event_type = record[0, 2].unpack1('S')
|
|
285
|
+
|
|
286
|
+
case event_type
|
|
287
|
+
when KEY_EVENT
|
|
288
|
+
parse_windows_key_record(record)
|
|
289
|
+
when MOUSE_EVENT
|
|
290
|
+
parse_windows_mouse_record(record)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def next_windows_pending_event
|
|
295
|
+
return nil unless @windows_pending_events && !@windows_pending_events.empty?
|
|
296
|
+
|
|
297
|
+
@windows_pending_events.shift
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def paste_seed_event?(event)
|
|
301
|
+
event && event[:type] == :key && [:string, :enter, :tab].include?(event[:name])
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def coalesce_windows_paste(handle, first_event)
|
|
305
|
+
events = [first_event]
|
|
306
|
+
sleep 0.002
|
|
307
|
+
available = windows_input_event_count(handle)
|
|
308
|
+
|
|
309
|
+
while available && available.positive?
|
|
310
|
+
record = Fiddle::Pointer.malloc(INPUT_RECORD_SIZE)
|
|
311
|
+
read_count = Fiddle::Pointer.malloc(4)
|
|
312
|
+
break unless read_console_input.call(handle, record, 1, read_count) != 0
|
|
313
|
+
break unless read_count[0, 4].unpack1('L') == 1
|
|
314
|
+
|
|
315
|
+
event = parse_windows_input_record(record)
|
|
316
|
+
if paste_seed_event?(event)
|
|
317
|
+
events << event
|
|
318
|
+
elsif event
|
|
319
|
+
(@windows_pending_events ||= []) << event
|
|
320
|
+
end
|
|
321
|
+
available = windows_input_event_count(handle)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
finish_windows_coalesced_events(events, first_event)
|
|
325
|
+
rescue
|
|
326
|
+
first_event
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def finish_windows_coalesced_events(events, first_event)
|
|
330
|
+
if windows_enter_burst?(events)
|
|
331
|
+
first_event
|
|
332
|
+
elsif windows_paste_burst?(events)
|
|
333
|
+
Event.key(:paste, value: events.map { |event| windows_paste_text(event) }.join)
|
|
334
|
+
else
|
|
335
|
+
(@windows_pending_events ||= []).concat(events[1..] || [])
|
|
336
|
+
first_event
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def windows_enter_burst?(events)
|
|
341
|
+
events.length <= 2 && events.all? { |event| event[:name] == :enter }
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def windows_paste_burst?(events)
|
|
345
|
+
return false if events.length <= 1
|
|
346
|
+
|
|
347
|
+
events.any? { |event| event[:name] == :enter } || events.length >= 4
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def windows_paste_text(event)
|
|
351
|
+
case event[:name]
|
|
352
|
+
when :string
|
|
353
|
+
event[:value].to_s
|
|
354
|
+
when :enter
|
|
355
|
+
"\n"
|
|
356
|
+
when :tab
|
|
357
|
+
"\t"
|
|
358
|
+
else
|
|
359
|
+
""
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def windows_input_event_count(handle)
|
|
364
|
+
count_ptr = Fiddle::Pointer.malloc(4)
|
|
365
|
+
return nil unless get_number_of_console_input_events.call(handle, count_ptr) != 0
|
|
366
|
+
|
|
367
|
+
count_ptr[0, 4].unpack1('L')
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def parse_windows_key_record(record)
|
|
371
|
+
key_down = record[4, 4].unpack1('L') != 0
|
|
372
|
+
return nil unless key_down
|
|
373
|
+
|
|
374
|
+
virtual_key = record[10, 2].unpack1('S')
|
|
375
|
+
control_state = record[12, 4].unpack1('L')
|
|
376
|
+
char_code = record[14, 2].unpack1('S')
|
|
377
|
+
char = char_code.zero? ? nil : char_code.chr(Encoding::UTF_8)
|
|
378
|
+
modifiers = windows_key_modifiers(control_state)
|
|
379
|
+
|
|
380
|
+
case virtual_key
|
|
381
|
+
when 13
|
|
382
|
+
return annotate_windows_key_event(Event.key(:alt_enter), virtual_key, char_code, control_state, modifiers) if modifiers.include?(:alt)
|
|
383
|
+
return annotate_windows_key_event(Event.key(:ctrl_enter), virtual_key, char_code, control_state, modifiers) if char_code == 10 && modifiers.include?(:ctrl)
|
|
384
|
+
return annotate_windows_key_event(Event.key(:shift_enter), virtual_key, char_code, control_state, modifiers) if char_code == 10
|
|
385
|
+
|
|
386
|
+
annotate_windows_key_event(Event.key(:enter), virtual_key, char_code, control_state, modifiers)
|
|
387
|
+
when 9
|
|
388
|
+
modifiers.include?(:shift) ? Event.key(:shift_tab) : Event.key(:tab)
|
|
389
|
+
when 8 then Event.key(:backspace)
|
|
390
|
+
when 27 then Event.key(:escape)
|
|
391
|
+
when 37 then Event.key(:left)
|
|
392
|
+
when 38 then Event.key(:up)
|
|
393
|
+
when 39 then Event.key(:right)
|
|
394
|
+
when 40 then Event.key(:down)
|
|
395
|
+
when 33 then Event.key(:page_up)
|
|
396
|
+
when 34 then Event.key(:page_down)
|
|
397
|
+
when 35 then Event.key(:end)
|
|
398
|
+
when 36 then Event.key(:home)
|
|
399
|
+
when 46 then Event.key(:delete)
|
|
400
|
+
else
|
|
401
|
+
ctrl_event = windows_ctrl_key_event(char_code)
|
|
402
|
+
return ctrl_event if ctrl_event
|
|
403
|
+
return Event.key(:ctrl_c) if char_code == 3
|
|
404
|
+
return nil unless char && !char.empty?
|
|
405
|
+
|
|
406
|
+
Event.key(:string, value: char)
|
|
407
|
+
end
|
|
408
|
+
rescue RangeError
|
|
409
|
+
nil
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def parse_windows_mouse_record(record)
|
|
413
|
+
raw_x = record[4, 2].unpack1('s')
|
|
414
|
+
raw_y = record[6, 2].unpack1('s')
|
|
415
|
+
button_state = record[8, 4].unpack1('L')
|
|
416
|
+
control_state = record[12, 4].unpack1('L')
|
|
417
|
+
event_flags = record[16, 4].unpack1('L')
|
|
418
|
+
|
|
419
|
+
if (event_flags & MOUSE_WHEELED) == MOUSE_WHEELED
|
|
420
|
+
delta = signed_high_word(button_state)
|
|
421
|
+
return Event.mouse(
|
|
422
|
+
:mouse_wheel,
|
|
423
|
+
button: :wheel,
|
|
424
|
+
x: raw_x,
|
|
425
|
+
y: raw_y,
|
|
426
|
+
raw_x: raw_x + 1,
|
|
427
|
+
raw_y: raw_y + 1,
|
|
428
|
+
code: button_state,
|
|
429
|
+
modifiers: windows_mouse_modifiers(control_state),
|
|
430
|
+
direction: delta.negative? ? :down : :up
|
|
431
|
+
)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
if (event_flags & MOUSE_MOVED) == MOUSE_MOVED
|
|
435
|
+
return nil if button_state.zero?
|
|
436
|
+
|
|
437
|
+
return windows_mouse_event(:mouse_drag, raw_x, raw_y, button_state, control_state)
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
if button_state.zero?
|
|
441
|
+
return windows_mouse_event(:mouse_up, raw_x, raw_y, button_state, control_state)
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
windows_mouse_event(:mouse_down, raw_x, raw_y, button_state, control_state)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def windows_mouse_event(name, raw_x, raw_y, button_state, control_state)
|
|
448
|
+
Event.mouse(
|
|
449
|
+
name,
|
|
450
|
+
button: windows_mouse_button(button_state),
|
|
451
|
+
x: raw_x,
|
|
452
|
+
y: raw_y,
|
|
453
|
+
raw_x: raw_x + 1,
|
|
454
|
+
raw_y: raw_y + 1,
|
|
455
|
+
code: button_state,
|
|
456
|
+
modifiers: windows_mouse_modifiers(control_state)
|
|
457
|
+
)
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def windows_mouse_button(button_state)
|
|
461
|
+
return :left if (button_state & FROM_LEFT_1ST_BUTTON_PRESSED) != 0
|
|
462
|
+
return :right if (button_state & RIGHTMOST_BUTTON_PRESSED) != 0
|
|
463
|
+
return :middle if (button_state & FROM_LEFT_2ND_BUTTON_PRESSED) != 0
|
|
464
|
+
|
|
465
|
+
:unknown
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def windows_mouse_modifiers(control_state)
|
|
469
|
+
windows_key_modifiers(control_state)
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def windows_key_modifiers(control_state)
|
|
473
|
+
modifiers = []
|
|
474
|
+
modifiers << :shift if (control_state & SHIFT_PRESSED) != 0
|
|
475
|
+
modifiers << :ctrl if (control_state & LEFT_CTRL_PRESSED) != 0 || (control_state & RIGHT_CTRL_PRESSED) != 0
|
|
476
|
+
modifiers << :alt if (control_state & LEFT_ALT_PRESSED) != 0 || (control_state & RIGHT_ALT_PRESSED) != 0
|
|
477
|
+
modifiers
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def windows_ctrl_key_event(char_code)
|
|
481
|
+
return nil unless char_code.between?(1, 26)
|
|
482
|
+
|
|
483
|
+
ctrl_char = (char_code + 64).chr.downcase
|
|
484
|
+
Event.key("ctrl_#{ctrl_char}".to_sym)
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def annotate_windows_key_event(event, virtual_key, char_code, control_state, modifiers)
|
|
488
|
+
return event unless @debug_input
|
|
489
|
+
|
|
490
|
+
event.merge(
|
|
491
|
+
raw: {
|
|
492
|
+
virtual_key: virtual_key,
|
|
493
|
+
char_code: char_code,
|
|
494
|
+
control_state: control_state,
|
|
495
|
+
modifiers: modifiers
|
|
496
|
+
}
|
|
497
|
+
)
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def signed_high_word(value)
|
|
501
|
+
high = (value >> 16) & 0xffff
|
|
502
|
+
high >= 0x8000 ? high - 0x10000 : high
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def ensure_fiddle
|
|
506
|
+
require 'fiddle'
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
end
|
data/lib/ruby_rich/text.rb
CHANGED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyRich
|
|
4
|
+
class Theme
|
|
5
|
+
DEFAULT_ROLES = {
|
|
6
|
+
accent: { color: :blue, bright: true, bold: true },
|
|
7
|
+
body: { color: :white, bright: true },
|
|
8
|
+
muted: { color: :black, bright: true },
|
|
9
|
+
dim: { color: :black, bright: true },
|
|
10
|
+
thinking: { color: :white, bright: true, italic: true },
|
|
11
|
+
success: { color: :green, bright: true },
|
|
12
|
+
warning: { color: :yellow, bright: true },
|
|
13
|
+
error: { color: :red, bright: true },
|
|
14
|
+
status: { color: :cyan, bright: true }
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
attr_reader :roles, :border, :focused_border
|
|
18
|
+
|
|
19
|
+
def self.auto
|
|
20
|
+
ENV["COLORFGBG"].to_s.split(";").last.to_i >= 8 ? light : agent_dark
|
|
21
|
+
rescue
|
|
22
|
+
agent_dark
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.agent_dark
|
|
26
|
+
new(
|
|
27
|
+
border: :blue,
|
|
28
|
+
focused_border: :cyan,
|
|
29
|
+
roles: {
|
|
30
|
+
accent: { color: :blue, bright: true, bold: true },
|
|
31
|
+
body: { color: :white, bright: true },
|
|
32
|
+
muted: { color: :black, bright: true },
|
|
33
|
+
dim: { color: :black, bright: true },
|
|
34
|
+
thinking: { color: :white, bright: true, italic: true },
|
|
35
|
+
success: { color: :green, bright: true },
|
|
36
|
+
warning: { color: :yellow, bright: true },
|
|
37
|
+
error: { color: :red, bright: true },
|
|
38
|
+
status: { color: :cyan, bright: true }
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.light
|
|
44
|
+
new(
|
|
45
|
+
border: :blue,
|
|
46
|
+
focused_border: :cyan,
|
|
47
|
+
roles: {
|
|
48
|
+
accent: { color: :blue, bright: false, bold: true },
|
|
49
|
+
body: { color: :black, bright: false },
|
|
50
|
+
muted: { color: :black, bright: true },
|
|
51
|
+
dim: { color: :black, bright: true },
|
|
52
|
+
success: { color: :green, bright: false },
|
|
53
|
+
warning: { color: :yellow, bright: false },
|
|
54
|
+
error: { color: :red, bright: false },
|
|
55
|
+
status: { color: :cyan, bright: false }
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.no_color
|
|
61
|
+
new(roles: DEFAULT_ROLES.transform_values { { color: :white } }, border: :white, focused_border: :white)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def initialize(roles: {}, border: :blue, focused_border: :cyan)
|
|
65
|
+
@roles = DEFAULT_ROLES.merge(roles)
|
|
66
|
+
@border = border
|
|
67
|
+
@focused_border = focused_border
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def style(text, role = :body)
|
|
71
|
+
options = @roles.fetch(role, @roles[:body])
|
|
72
|
+
"#{style_code(options)}#{text}#{AnsiCode.reset}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def color(name, bright: false)
|
|
76
|
+
AnsiCode.color(name, bright)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def panel_border(focused: false)
|
|
80
|
+
focused ? @focused_border : @border
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def style_code(options)
|
|
86
|
+
code = AnsiCode.font(
|
|
87
|
+
options.fetch(:color, :white),
|
|
88
|
+
font_bright: options.fetch(:bright, false),
|
|
89
|
+
bold: options[:bold],
|
|
90
|
+
italic: options[:italic]
|
|
91
|
+
)
|
|
92
|
+
code += AnsiCode.faint if options[:faint]
|
|
93
|
+
code
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyRich
|
|
4
|
+
class ToolBlock
|
|
5
|
+
STATES = [:pending, :running, :done, :error, :cancelled, :denied].freeze
|
|
6
|
+
|
|
7
|
+
def initialize(entry, width: 80)
|
|
8
|
+
@entry = entry
|
|
9
|
+
@width = [width, 20].max
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def render
|
|
13
|
+
name = @entry.name || @entry.metadata[:name] || "tool"
|
|
14
|
+
status = normalize_status(@entry.status || :pending)
|
|
15
|
+
header = "#{AnsiCode.color(color(status), true)}• #{status_marker(status)} #{name} #{status}#{AnsiCode.reset}"
|
|
16
|
+
return [header, " #{summary(@entry.content)}", " details collapsed; press Ctrl+O for full output"] if @entry.collapsed
|
|
17
|
+
|
|
18
|
+
[header] + details(@entry.content)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def normalize_status(status)
|
|
24
|
+
status = status.to_sym
|
|
25
|
+
return :error if status == :failed || status == :issue
|
|
26
|
+
return status if STATES.include?(status)
|
|
27
|
+
|
|
28
|
+
:pending
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def color(status)
|
|
32
|
+
case status
|
|
33
|
+
when :done then :green
|
|
34
|
+
when :error, :denied then :red
|
|
35
|
+
when :cancelled then :yellow
|
|
36
|
+
else :blue
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def status_marker(status)
|
|
41
|
+
case status
|
|
42
|
+
when :done then "✓"
|
|
43
|
+
when :error then "!"
|
|
44
|
+
when :cancelled then "-"
|
|
45
|
+
when :denied then "x"
|
|
46
|
+
else "▸"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def summary(content)
|
|
51
|
+
plain = content.to_s.gsub(/\e\[[0-9;:]*m/, "").split("\n").first.to_s
|
|
52
|
+
plain.empty? ? "<no output>" : plain[0, [@width - 4, 20].max]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def details(content)
|
|
56
|
+
text = content.to_s
|
|
57
|
+
return [" <no output>"] if text.empty?
|
|
58
|
+
|
|
59
|
+
text.split("\n").flat_map { |line| wrap(line).map { |part| " #{part}" } }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def wrap(line)
|
|
63
|
+
max_width = [@width - 2, 20].max
|
|
64
|
+
result = []
|
|
65
|
+
current = +""
|
|
66
|
+
width = 0
|
|
67
|
+
in_escape = false
|
|
68
|
+
line.each_char do |char|
|
|
69
|
+
if in_escape
|
|
70
|
+
current << char
|
|
71
|
+
in_escape = false if char == "m"
|
|
72
|
+
next
|
|
73
|
+
elsif char.ord == 27
|
|
74
|
+
current << char
|
|
75
|
+
in_escape = true
|
|
76
|
+
next
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
char_width = Unicode::DisplayWidth.of(char)
|
|
80
|
+
if width + char_width > max_width
|
|
81
|
+
result << current
|
|
82
|
+
current = +""
|
|
83
|
+
width = 0
|
|
84
|
+
end
|
|
85
|
+
current << char
|
|
86
|
+
width += char_width
|
|
87
|
+
end
|
|
88
|
+
result << current unless current.empty?
|
|
89
|
+
result
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|