ruby_rich 0.4.0 → 0.4.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/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 +32 -25
data/lib/ruby_rich/console.rb
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
require 'io/console'
|
|
2
|
+
require_relative 'event'
|
|
3
|
+
require_relative 'terminal'
|
|
2
4
|
|
|
3
5
|
module RubyRich
|
|
4
6
|
class Console
|
|
@@ -18,7 +20,25 @@ module RubyRich
|
|
|
18
20
|
# 其他
|
|
19
21
|
'[5~' => :page_up, '[6~' => :page_down,
|
|
20
22
|
'[H' => :home, '[F' => :end,
|
|
21
|
-
'[2~' => :insert, '[3~' => :delete
|
|
23
|
+
'[2~' => :insert, '[3~' => :delete,
|
|
24
|
+
'[Z' => :shift_tab,
|
|
25
|
+
'[13;2u' => :shift_enter,
|
|
26
|
+
'[13;3u' => :alt_enter,
|
|
27
|
+
'[13;2~' => :shift_enter,
|
|
28
|
+
'[13;3~' => :alt_enter
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
WINDOWS_SPECIAL_KEYS = {
|
|
32
|
+
72 => :up,
|
|
33
|
+
80 => :down,
|
|
34
|
+
75 => :left,
|
|
35
|
+
77 => :right,
|
|
36
|
+
71 => :home,
|
|
37
|
+
79 => :end,
|
|
38
|
+
73 => :page_up,
|
|
39
|
+
81 => :page_down,
|
|
40
|
+
82 => :insert,
|
|
41
|
+
83 => :delete
|
|
22
42
|
}.freeze
|
|
23
43
|
|
|
24
44
|
def initialize
|
|
@@ -86,45 +106,104 @@ module RubyRich
|
|
|
86
106
|
system('clear')
|
|
87
107
|
end
|
|
88
108
|
|
|
109
|
+
def get_event(input: $stdin)
|
|
110
|
+
if RubyRich::Terminal.windows_mouse_reporting?
|
|
111
|
+
event = RubyRich::Terminal.read_windows_input_event
|
|
112
|
+
return event if event
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
get_key(input: input)
|
|
116
|
+
end
|
|
117
|
+
|
|
89
118
|
def get_key(input: $stdin)
|
|
90
119
|
input.raw(intr: true) do |io|
|
|
120
|
+
RubyRich::Terminal.prepare_input
|
|
91
121
|
char = io.getch
|
|
92
|
-
|
|
122
|
+
bytes = char.b.bytes
|
|
123
|
+
byte = bytes.first
|
|
124
|
+
|
|
125
|
+
return Event.key(:ctrl_c) if byte == 3
|
|
126
|
+
|
|
127
|
+
# Handle Enter key first (ASCII 13 = \r, ASCII 10 = \n)
|
|
93
128
|
if char == "\r" || char == "\n"
|
|
94
|
-
#
|
|
129
|
+
# Check for subsequent input (pasted content has multiple characters)
|
|
95
130
|
has_more = IO.select([io], nil, nil, 0)
|
|
96
|
-
|
|
131
|
+
|
|
132
|
+
return has_more ? Event.key(:paste, value: collect_pending_input(io, char)) : Event.key(:enter)
|
|
97
133
|
end
|
|
98
|
-
#
|
|
134
|
+
# Handle Tab key separately (ASCII 9)
|
|
99
135
|
if char == "\t"
|
|
100
|
-
return
|
|
101
|
-
elsif
|
|
102
|
-
return
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
136
|
+
return Event.key(:tab)
|
|
137
|
+
elsif byte == 8 || byte == 0x7F
|
|
138
|
+
return Event.key(:backspace)
|
|
139
|
+
# Windows special keys can be delivered as:
|
|
140
|
+
# - "\xE0" + a second getch byte
|
|
141
|
+
# - "\xE0H" in one read
|
|
142
|
+
elsif byte == 0 || byte == 224
|
|
143
|
+
code = bytes[1]
|
|
144
|
+
code ||= io.getch.b.bytes.first
|
|
145
|
+
return Event.key(WINDOWS_SPECIAL_KEYS[code] || :"windows_special_#{code}")
|
|
146
|
+
elsif char == "\e" # Detect escape sequence
|
|
147
|
+
sequence = char.b.bytes.drop(1).pack('C*')
|
|
148
|
+
|
|
149
|
+
sleep 0.01
|
|
150
|
+
while IO.select([io], nil, nil, 0)
|
|
151
|
+
next_char = io.getch
|
|
152
|
+
break unless next_char
|
|
153
|
+
|
|
154
|
+
sequence << next_char
|
|
155
|
+
return read_bracketed_paste(io) if sequence == "[200~"
|
|
156
|
+
break if complete_escape_sequence?(sequence)
|
|
112
157
|
end
|
|
158
|
+
|
|
113
159
|
if sequence.empty?
|
|
114
|
-
return
|
|
160
|
+
return Event.key(:escape)
|
|
161
|
+
elsif sequence == "\r" || sequence == "\n"
|
|
162
|
+
return Event.key(:alt_enter)
|
|
163
|
+
elsif (mouse_event = parse_sgr_mouse(sequence))
|
|
164
|
+
return mouse_event
|
|
115
165
|
else
|
|
116
|
-
return
|
|
166
|
+
return Event.key(ESCAPE_SEQUENCES[sequence] || :"ansi_#{sequence.inspect}")
|
|
117
167
|
end
|
|
118
|
-
#
|
|
119
|
-
elsif
|
|
120
|
-
ctrl_char = (
|
|
121
|
-
return
|
|
168
|
+
# Handle Ctrl combinations (excluding Tab and Enter)
|
|
169
|
+
elsif byte.between?(1, 8) || byte.between?(10, 26)
|
|
170
|
+
ctrl_char = (byte + 64).chr.downcase
|
|
171
|
+
return Event.key("ctrl_#{ctrl_char}".to_sym)
|
|
122
172
|
else
|
|
123
|
-
|
|
173
|
+
if IO.select([io], nil, nil, 0)
|
|
174
|
+
Event.key(:paste, value: collect_pending_input(io, char))
|
|
175
|
+
else
|
|
176
|
+
Event.key(:string, value: char)
|
|
177
|
+
end
|
|
124
178
|
end
|
|
125
179
|
end
|
|
126
180
|
end
|
|
127
181
|
|
|
182
|
+
def parse_sgr_mouse(sequence)
|
|
183
|
+
match = sequence.match(/\A\[<(\d+);(\d+);(\d+)([Mm])\z/)
|
|
184
|
+
return nil unless match
|
|
185
|
+
|
|
186
|
+
code = match[1].to_i
|
|
187
|
+
raw_x = match[2].to_i
|
|
188
|
+
raw_y = match[3].to_i
|
|
189
|
+
terminator = match[4]
|
|
190
|
+
button = mouse_button(code)
|
|
191
|
+
name = mouse_event_name(code, terminator)
|
|
192
|
+
direction = mouse_wheel_direction(code)
|
|
193
|
+
|
|
194
|
+
Event.mouse(
|
|
195
|
+
name,
|
|
196
|
+
button: button,
|
|
197
|
+
x: raw_x - 1,
|
|
198
|
+
y: raw_y - 1,
|
|
199
|
+
raw_x: raw_x,
|
|
200
|
+
raw_y: raw_y,
|
|
201
|
+
code: code,
|
|
202
|
+
modifiers: mouse_modifiers(code),
|
|
203
|
+
direction: direction
|
|
204
|
+
)
|
|
205
|
+
end
|
|
206
|
+
|
|
128
207
|
def add_line(text)
|
|
129
208
|
@lines << text
|
|
130
209
|
end
|
|
@@ -151,6 +230,76 @@ module RubyRich
|
|
|
151
230
|
|
|
152
231
|
private
|
|
153
232
|
|
|
233
|
+
def read_bracketed_paste(io)
|
|
234
|
+
terminator = "\e[201~"
|
|
235
|
+
buffer = +""
|
|
236
|
+
loop do
|
|
237
|
+
char = io.getch
|
|
238
|
+
break unless char
|
|
239
|
+
|
|
240
|
+
buffer << char
|
|
241
|
+
break if buffer.end_with?(terminator)
|
|
242
|
+
end
|
|
243
|
+
buffer = normalize_paste_text(buffer.delete_suffix(terminator))
|
|
244
|
+
Event.key(:paste, value: buffer)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def collect_pending_input(io, first_char)
|
|
248
|
+
buffer = +first_char
|
|
249
|
+
while IO.select([io], nil, nil, 0)
|
|
250
|
+
char = io.getch
|
|
251
|
+
break unless char
|
|
252
|
+
|
|
253
|
+
buffer << char
|
|
254
|
+
end
|
|
255
|
+
normalize_paste_text(buffer)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def normalize_paste_text(text)
|
|
259
|
+
text.to_s.gsub(/\r\n?/, "\n")
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def complete_escape_sequence?(sequence)
|
|
263
|
+
if sequence.start_with?('[<')
|
|
264
|
+
sequence.end_with?('M') || sequence.end_with?('m') || sequence.length >= 32
|
|
265
|
+
else
|
|
266
|
+
ESCAPE_SEQUENCES.key?(sequence) || sequence.length >= 8
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def mouse_event_name(code, terminator)
|
|
271
|
+
return :mouse_up if terminator == 'm'
|
|
272
|
+
return :mouse_wheel if (code & 64) == 64
|
|
273
|
+
return :mouse_drag if (code & 32) == 32
|
|
274
|
+
|
|
275
|
+
:mouse_down
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def mouse_button(code)
|
|
279
|
+
return :wheel if (code & 64) == 64
|
|
280
|
+
|
|
281
|
+
case code & 3
|
|
282
|
+
when 0 then :left
|
|
283
|
+
when 1 then :middle
|
|
284
|
+
when 2 then :right
|
|
285
|
+
else :unknown
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def mouse_wheel_direction(code)
|
|
290
|
+
return nil unless (code & 64) == 64
|
|
291
|
+
|
|
292
|
+
(code & 1) == 1 ? :down : :up
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def mouse_modifiers(code)
|
|
296
|
+
modifiers = []
|
|
297
|
+
modifiers << :shift if (code & 4) == 4
|
|
298
|
+
modifiers << :alt if (code & 8) == 8
|
|
299
|
+
modifiers << :ctrl if (code & 16) == 16
|
|
300
|
+
modifiers
|
|
301
|
+
end
|
|
302
|
+
|
|
154
303
|
def format_line(line)
|
|
155
304
|
content = line.is_a?(RichText) ? line.render : line
|
|
156
305
|
case @layout[:align]
|
|
@@ -163,4 +312,4 @@ module RubyRich
|
|
|
163
312
|
end
|
|
164
313
|
end
|
|
165
314
|
end
|
|
166
|
-
end
|
|
315
|
+
end
|
data/lib/ruby_rich/dialog.rb
CHANGED
|
@@ -4,6 +4,7 @@ module RubyRich
|
|
|
4
4
|
attr_accessor :width, :height
|
|
5
5
|
|
|
6
6
|
def initialize(title: "", content: "", width: 48, height: 8, buttons: [:ok])
|
|
7
|
+
@title = title
|
|
7
8
|
@width = width
|
|
8
9
|
@height = height
|
|
9
10
|
terminal_width = `tput cols`.to_i
|
|
@@ -63,4 +64,4 @@ module RubyRich
|
|
|
63
64
|
end
|
|
64
65
|
end
|
|
65
66
|
end
|
|
66
|
-
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyRich
|
|
4
|
+
module Event
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def key(name, value: nil)
|
|
8
|
+
event = { name: name, type: :key }
|
|
9
|
+
event[:value] = value unless value.nil?
|
|
10
|
+
event
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def mouse(name, button:, x:, y:, raw_x:, raw_y:, code:, modifiers: [], direction: nil)
|
|
14
|
+
event = {
|
|
15
|
+
name: name,
|
|
16
|
+
type: :mouse,
|
|
17
|
+
button: button,
|
|
18
|
+
x: x,
|
|
19
|
+
y: y,
|
|
20
|
+
raw_x: raw_x,
|
|
21
|
+
raw_y: raw_y,
|
|
22
|
+
code: code,
|
|
23
|
+
modifiers: modifiers
|
|
24
|
+
}
|
|
25
|
+
event[:direction] = direction if direction
|
|
26
|
+
event
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyRich
|
|
4
|
+
class FocusManager
|
|
5
|
+
attr_reader :focused_name
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@entries = []
|
|
9
|
+
@focused_name = nil
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def register(name, layout, target)
|
|
13
|
+
@entries << { name: name, layout: layout, target: target }
|
|
14
|
+
focus(name) unless @focused_name
|
|
15
|
+
self
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def attach(root_layout, priority: 500)
|
|
19
|
+
root_layout.key(:tab, priority) do |_event_data, _live|
|
|
20
|
+
current = @entries.find { |entry| entry[:name] == @focused_name }
|
|
21
|
+
if current && current[:target].respond_to?(:wants_tab?) && current[:target].wants_tab?
|
|
22
|
+
false
|
|
23
|
+
else
|
|
24
|
+
next_entry = focus_next
|
|
25
|
+
target = next_entry && next_entry[:target]
|
|
26
|
+
target.ignore_next_tab if target.respond_to?(:ignore_next_tab)
|
|
27
|
+
|
|
28
|
+
false
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
root_layout.key(:mouse_down, priority) do |event_data, _live|
|
|
33
|
+
entry = entry_at(event_data[:x], event_data[:y])
|
|
34
|
+
if entry
|
|
35
|
+
focus(entry[:name])
|
|
36
|
+
false
|
|
37
|
+
else
|
|
38
|
+
false
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def focus(name)
|
|
46
|
+
entry = @entries.find { |item| item[:name] == name }
|
|
47
|
+
return nil unless entry
|
|
48
|
+
|
|
49
|
+
blur_current
|
|
50
|
+
@focused_name = entry[:name]
|
|
51
|
+
entry[:target].focus if entry[:target].respond_to?(:focus)
|
|
52
|
+
entry
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def focus_next
|
|
56
|
+
return nil if @entries.empty?
|
|
57
|
+
|
|
58
|
+
index = @entries.index { |entry| entry[:name] == @focused_name } || -1
|
|
59
|
+
focus(@entries[(index + 1) % @entries.length][:name])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def focused?(name)
|
|
63
|
+
@focused_name == name
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def blur_current
|
|
69
|
+
current = @entries.find { |entry| entry[:name] == @focused_name }
|
|
70
|
+
current[:target].blur if current && current[:target].respond_to?(:blur)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def entry_at(x, y)
|
|
74
|
+
@entries.reverse.find { |entry| entry[:layout].contains?(x, y) }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
data/lib/ruby_rich/layout.rb
CHANGED
|
@@ -19,6 +19,7 @@ module RubyRich
|
|
|
19
19
|
@show = true
|
|
20
20
|
@event_listeners = {}
|
|
21
21
|
@event_intercepted = false
|
|
22
|
+
@mouse_capture = nil
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
def key(event_name, priority = 0, &block)
|
|
@@ -35,6 +36,8 @@ module RubyRich
|
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
def notify_listeners(event_data)
|
|
39
|
+
return route_mouse_event(event_data) if event_data[:type] == :mouse
|
|
40
|
+
|
|
38
41
|
return if @event_intercepted
|
|
39
42
|
if @dialog
|
|
40
43
|
@dialog.notify_listeners(event_data)
|
|
@@ -62,6 +65,27 @@ module RubyRich
|
|
|
62
65
|
@parent ? @parent.root : self
|
|
63
66
|
end
|
|
64
67
|
|
|
68
|
+
def contains?(x, y)
|
|
69
|
+
return false unless @show
|
|
70
|
+
return false unless @width && @height
|
|
71
|
+
|
|
72
|
+
x >= @x_offset &&
|
|
73
|
+
x < @x_offset + @width &&
|
|
74
|
+
y >= @y_offset &&
|
|
75
|
+
y < @y_offset + @height
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def hit_test(x, y)
|
|
79
|
+
return nil unless contains?(x, y)
|
|
80
|
+
|
|
81
|
+
@children.reverse_each do |child|
|
|
82
|
+
hit = child.hit_test(x, y)
|
|
83
|
+
return hit if hit
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
self
|
|
87
|
+
end
|
|
88
|
+
|
|
65
89
|
def split_row(*layouts)
|
|
66
90
|
@split_direction = :row
|
|
67
91
|
layouts.each { |l| l.parent = self }
|
|
@@ -75,6 +99,7 @@ module RubyRich
|
|
|
75
99
|
end
|
|
76
100
|
|
|
77
101
|
def add_child(layout)
|
|
102
|
+
layout.parent = self
|
|
78
103
|
@children << layout
|
|
79
104
|
end
|
|
80
105
|
|
|
@@ -111,15 +136,15 @@ module RubyRich
|
|
|
111
136
|
end
|
|
112
137
|
|
|
113
138
|
def render
|
|
114
|
-
#
|
|
139
|
+
# Convert buffer to string (join lines with newlines)
|
|
115
140
|
buffer = render_to_buffer
|
|
116
141
|
buffer.map { |line| line.compact.join("") }.join("\n")
|
|
117
142
|
end
|
|
118
143
|
|
|
119
144
|
def render_to_buffer
|
|
120
|
-
#
|
|
145
|
+
# Initialize buffer (2D array, each element represents a character)
|
|
121
146
|
buffer = Array.new(@height) { Array.new(@width, " ") }
|
|
122
|
-
#
|
|
147
|
+
# Recursively fill content into buffer
|
|
123
148
|
render_into(buffer)
|
|
124
149
|
render_dialog_into(buffer) if @dialog
|
|
125
150
|
return buffer
|
|
@@ -150,7 +175,7 @@ module RubyRich
|
|
|
150
175
|
content_lines = if content.is_a?(String)
|
|
151
176
|
content.split("\n")[0...height]
|
|
152
177
|
else
|
|
153
|
-
content.render[0...height]
|
|
178
|
+
normalize_rendered_lines(content.render)[0...height]
|
|
154
179
|
end
|
|
155
180
|
|
|
156
181
|
content_lines.each_with_index do |line, line_index|
|
|
@@ -159,9 +184,9 @@ module RubyRich
|
|
|
159
184
|
|
|
160
185
|
in_escape = false
|
|
161
186
|
escape_char = ""
|
|
162
|
-
char_width = 0 #
|
|
187
|
+
char_width = 0 # Initialize width to 0 for position calculation
|
|
163
188
|
line.each_char do |char|
|
|
164
|
-
#
|
|
189
|
+
# Handle ANSI escape codes
|
|
165
190
|
if in_escape
|
|
166
191
|
escape_char += char
|
|
167
192
|
in_escape = false if char == 'm'
|
|
@@ -169,49 +194,47 @@ module RubyRich
|
|
|
169
194
|
escape_char = ""
|
|
170
195
|
end
|
|
171
196
|
next
|
|
172
|
-
elsif char.ord == 27 #
|
|
197
|
+
elsif char.ord == 27 # Detect escape sequence start \e
|
|
173
198
|
in_escape = true
|
|
174
199
|
escape_char += char
|
|
175
200
|
next
|
|
176
201
|
end
|
|
177
|
-
|
|
178
|
-
#
|
|
202
|
+
|
|
203
|
+
# Calculate character width
|
|
179
204
|
char_w = case char.ord
|
|
180
|
-
when 0x0000..0x007F then 1 #
|
|
181
|
-
when 0x4E00..0x9FFF then 2 #
|
|
205
|
+
when 0x0000..0x007F then 1 # English characters
|
|
206
|
+
when 0x4E00..0x9FFF then 2 # Chinese characters
|
|
182
207
|
else Unicode::DisplayWidth.of(char)
|
|
183
208
|
end
|
|
184
|
-
#
|
|
209
|
+
# Calculate character start position
|
|
185
210
|
x_start = x_offset + char_width
|
|
186
|
-
|
|
187
|
-
#
|
|
211
|
+
|
|
212
|
+
# Skip if beyond right boundary
|
|
188
213
|
next if x_start >= buffer[y_pos].size
|
|
189
|
-
|
|
190
|
-
#
|
|
214
|
+
|
|
215
|
+
# Handle character rendering (Chinese characters may occupy multiple positions)
|
|
191
216
|
char_w.times do |i|
|
|
192
217
|
x_pos = x_start + i
|
|
193
|
-
break if x_pos >= buffer[y_pos].size #
|
|
218
|
+
break if x_pos >= buffer[y_pos].size # Stop at right boundary
|
|
194
219
|
unless escape_char.empty?
|
|
195
|
-
char = escape_char + char + "\e[0m" #
|
|
220
|
+
char = escape_char + char + "\e[0m" # Record character's actual color each time
|
|
196
221
|
end
|
|
197
|
-
buffer[y_pos][x_pos] = char unless i > 0 #
|
|
222
|
+
buffer[y_pos][x_pos] = char unless i > 0 # Write Chinese character only at first position to avoid overwriting
|
|
198
223
|
buffer[y_pos][x_pos+1] = nil if char_w == 2
|
|
199
224
|
end
|
|
200
|
-
char_width += char_w #
|
|
225
|
+
char_width += char_w # Update cumulative width
|
|
201
226
|
end
|
|
202
227
|
end
|
|
203
228
|
end
|
|
204
229
|
|
|
205
230
|
def calculate_node_dimensions(available_width, available_height)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
end
|
|
231
|
+
@width = if @size && @parent&.split_direction == :row
|
|
232
|
+
[@size, available_width].min
|
|
233
|
+
else
|
|
234
|
+
available_width
|
|
235
|
+
end
|
|
212
236
|
|
|
213
|
-
|
|
214
|
-
@height ||= if @size
|
|
237
|
+
@height = if @size && @parent&.split_direction == :column
|
|
215
238
|
[@size, available_height].min
|
|
216
239
|
else
|
|
217
240
|
available_height
|
|
@@ -220,10 +243,20 @@ module RubyRich
|
|
|
220
243
|
if @content.class == RubyRich::Panel
|
|
221
244
|
@content.width = @width
|
|
222
245
|
@content.height = @height
|
|
246
|
+
else
|
|
247
|
+
@content.width = @width if @content.respond_to?(:width=)
|
|
248
|
+
@content.height = @height if @content.respond_to?(:height=)
|
|
223
249
|
end
|
|
224
250
|
|
|
225
251
|
return if @children.empty?
|
|
226
252
|
|
|
253
|
+
@children.each do |child|
|
|
254
|
+
next unless child.content.respond_to?(:desired_height)
|
|
255
|
+
next unless @split_direction == :column
|
|
256
|
+
|
|
257
|
+
child.size = [[child.content.desired_height, 1].max, available_height - 2].min
|
|
258
|
+
end
|
|
259
|
+
|
|
227
260
|
case @split_direction
|
|
228
261
|
when :row
|
|
229
262
|
remaining_width = @width
|
|
@@ -290,5 +323,60 @@ module RubyRich
|
|
|
290
323
|
end
|
|
291
324
|
end
|
|
292
325
|
end
|
|
326
|
+
|
|
327
|
+
private
|
|
328
|
+
|
|
329
|
+
def route_mouse_event(event_data)
|
|
330
|
+
if @dialog
|
|
331
|
+
@dialog.notify_listeners(event_data)
|
|
332
|
+
return
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
capture = root.instance_variable_get(:@mouse_capture)
|
|
336
|
+
target = capture && [:mouse_drag, :mouse_up].include?(event_data[:name]) ? capture : (hit_test(event_data[:x], event_data[:y]) || self)
|
|
337
|
+
handled = target.bubble_mouse_event(event_data)
|
|
338
|
+
|
|
339
|
+
root.instance_variable_set(:@mouse_capture, target) if event_data[:name] == :mouse_down && handled
|
|
340
|
+
root.instance_variable_set(:@mouse_capture, nil) if event_data[:name] == :mouse_up
|
|
341
|
+
handled
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
protected
|
|
345
|
+
|
|
346
|
+
def bubble_mouse_event(event_data)
|
|
347
|
+
current = self
|
|
348
|
+
while current
|
|
349
|
+
return true if current.dispatch_event_listeners(event_data)
|
|
350
|
+
|
|
351
|
+
current = current.parent
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
false
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def dispatch_event_listeners(event_data)
|
|
358
|
+
listeners = @event_listeners[event_data[:name]]
|
|
359
|
+
return false unless listeners
|
|
360
|
+
|
|
361
|
+
listeners.each do |listener|
|
|
362
|
+
result = listener[:block].call(event_data, root.live)
|
|
363
|
+
return true if result == true
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
false
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
private
|
|
370
|
+
|
|
371
|
+
def normalize_rendered_lines(rendered)
|
|
372
|
+
case rendered
|
|
373
|
+
when String
|
|
374
|
+
rendered.split("\n")
|
|
375
|
+
when Array
|
|
376
|
+
rendered
|
|
377
|
+
else
|
|
378
|
+
rendered.to_s.split("\n")
|
|
379
|
+
end
|
|
380
|
+
end
|
|
293
381
|
end
|
|
294
|
-
end
|
|
382
|
+
end
|