ruby_rich 0.4.3 → 0.4.5
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 +1 -1
- data/lib/ruby_rich/app_shell.rb +9 -3
- data/lib/ruby_rich/composer.rb +24 -1
- data/lib/ruby_rich/focus_manager.rb +13 -2
- data/lib/ruby_rich/layout.rb +11 -0
- data/lib/ruby_rich/line_editor.rb +118 -47
- data/lib/ruby_rich/live.rb +120 -18
- data/lib/ruby_rich/panel.rb +21 -2
- data/lib/ruby_rich/table.rb +21 -13
- data/lib/ruby_rich/terminal.rb +1 -1
- data/lib/ruby_rich/transcript.rb +13 -1
- data/lib/ruby_rich/version.rb +1 -1
- data/lib/ruby_rich/viewport.rb +102 -8
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fa1b6881cd40609ff69b7e2b4c766c5fa3551bf49cd5558ad79681d89abc4e1a
|
|
4
|
+
data.tar.gz: e596c29715037aca70508b3dc564b009bc20bfa8f56a6286fb173819f9b2750a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 425b8e8584af060a2e9eb1ffe7666e0f2ddcec84c4a6a8005ffaf0f44c976b3e9c4c121a839a7c2b617f26ccabac20db8ef93ec1b3bc163761bd538a689e8f93
|
|
7
|
+
data.tar.gz: f17b46fb04ef923a0803163b06be590f985187ac50a81e9d76a47329ce2503c353c98f7d90295a94a534ecefe201907d0e6aa2585764c8a76df99f2ce436bf62
|
|
@@ -42,7 +42,7 @@ module RubyRich
|
|
|
42
42
|
self
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
def start(refresh_rate: 24, mouse: true, alt_screen:
|
|
45
|
+
def start(refresh_rate: 24, mouse: true, alt_screen: true)
|
|
46
46
|
@state_mutex.synchronize { @state = :starting }
|
|
47
47
|
Live.start(@layout, refresh_rate: refresh_rate, mouse: mouse, alt_screen: alt_screen, autowrap: false) do |live|
|
|
48
48
|
@state_mutex.synchronize do
|
data/lib/ruby_rich/app_shell.rb
CHANGED
|
@@ -23,11 +23,11 @@ module RubyRich
|
|
|
23
23
|
@progress_text = nil
|
|
24
24
|
|
|
25
25
|
@transcript = Transcript.new
|
|
26
|
-
@progress_manager = ProgressManager.new(on_change: ->(text) {
|
|
26
|
+
@progress_manager = ProgressManager.new(on_change: ->(text) { update_progress_text(text) })
|
|
27
27
|
@viewport = Viewport.new(@transcript, scrollbar: true, auto_scroll: true)
|
|
28
28
|
@sidebar = Sidebar.new
|
|
29
29
|
@composer = Composer.new(
|
|
30
|
-
placeholder: "
|
|
30
|
+
placeholder: "Create a task or enter /",
|
|
31
31
|
commands: commands,
|
|
32
32
|
on_submit: method(:handle_submit),
|
|
33
33
|
on_select: method(:handle_select)
|
|
@@ -104,6 +104,12 @@ module RubyRich
|
|
|
104
104
|
self
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
+
def update_progress_text(text)
|
|
108
|
+
@progress_text = text
|
|
109
|
+
@live&.refresh
|
|
110
|
+
self
|
|
111
|
+
end
|
|
112
|
+
|
|
107
113
|
def start_progress(message = nil, owner: Thread.current.object_id, style: :primary, quiet_on_fast_finish: false)
|
|
108
114
|
_ = style
|
|
109
115
|
_ = quiet_on_fast_finish
|
|
@@ -141,7 +147,7 @@ module RubyRich
|
|
|
141
147
|
false
|
|
142
148
|
end
|
|
143
149
|
|
|
144
|
-
def start(refresh_rate: 24, mouse: true, alt_screen:
|
|
150
|
+
def start(refresh_rate: 24, mouse: true, alt_screen: true)
|
|
145
151
|
Live.start(@layout, refresh_rate: refresh_rate, mouse: mouse, alt_screen: alt_screen, autowrap: false) do |live|
|
|
146
152
|
@live = live
|
|
147
153
|
live.listening = true
|
data/lib/ruby_rich/composer.rb
CHANGED
|
@@ -73,6 +73,27 @@ module RubyRich
|
|
|
73
73
|
@menu_open
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
+
def native_cursor_position
|
|
77
|
+
return nil unless focused?
|
|
78
|
+
|
|
79
|
+
input_width = [inner_width - 2, 1].max
|
|
80
|
+
editor_row, editor_col = @editor.cursor_visual_position(width: input_width)
|
|
81
|
+
raw_row = [@attachments.length, 3].min + editor_row
|
|
82
|
+
raw_col = 2 + editor_col
|
|
83
|
+
|
|
84
|
+
raw_height = [@height.to_i, 1].max
|
|
85
|
+
raw_lines = []
|
|
86
|
+
raw_lines.concat(render_attachments)
|
|
87
|
+
raw_lines.concat(render_input_lines)
|
|
88
|
+
raw_lines.concat(render_menu_lines) if menu_open?
|
|
89
|
+
|
|
90
|
+
clipped_rows = [raw_lines.length - raw_height, 0].max
|
|
91
|
+
visible_row = raw_row - clipped_rows
|
|
92
|
+
return nil if visible_row.negative? || visible_row >= raw_height
|
|
93
|
+
|
|
94
|
+
[visible_row, raw_col]
|
|
95
|
+
end
|
|
96
|
+
|
|
76
97
|
def wants_tab?
|
|
77
98
|
focused? && (menu_open? || @editor.value.include?("/"))
|
|
78
99
|
end
|
|
@@ -229,7 +250,7 @@ module RubyRich
|
|
|
229
250
|
elsif !@editor.empty?
|
|
230
251
|
@editor.clear
|
|
231
252
|
else
|
|
232
|
-
|
|
253
|
+
focus
|
|
233
254
|
end
|
|
234
255
|
end
|
|
235
256
|
|
|
@@ -433,6 +454,8 @@ module RubyRich
|
|
|
433
454
|
def colorize_input(line)
|
|
434
455
|
color = AnsiCode.color(:white, true)
|
|
435
456
|
reset = AnsiCode.reset
|
|
457
|
+
return line.to_s if line.to_s.include?(AnsiCode.inverse)
|
|
458
|
+
|
|
436
459
|
"#{color}#{line.to_s.gsub(reset, "#{reset}#{color}")}#{reset}"
|
|
437
460
|
end
|
|
438
461
|
|
|
@@ -29,8 +29,8 @@ module RubyRich
|
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
root_layout.key(:
|
|
33
|
-
entry = entry_at(event_data[:x], event_data[:y])
|
|
32
|
+
root_layout.key(:mouse_target, priority) do |event_data, _live|
|
|
33
|
+
entry = entry_for_layout(event_data[:target_layout]) || entry_at(event_data[:x], event_data[:y])
|
|
34
34
|
if entry
|
|
35
35
|
focus(entry[:name])
|
|
36
36
|
false
|
|
@@ -73,5 +73,16 @@ module RubyRich
|
|
|
73
73
|
def entry_at(x, y)
|
|
74
74
|
@entries.reverse.find { |entry| entry[:layout].contains?(x, y) }
|
|
75
75
|
end
|
|
76
|
+
|
|
77
|
+
def entry_for_layout(layout)
|
|
78
|
+
current = layout
|
|
79
|
+
while current
|
|
80
|
+
entry = @entries.reverse.find { |item| item[:layout] == current }
|
|
81
|
+
return entry if entry
|
|
82
|
+
|
|
83
|
+
current = current.parent
|
|
84
|
+
end
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
76
87
|
end
|
|
77
88
|
end
|
data/lib/ruby_rich/layout.rb
CHANGED
|
@@ -334,6 +334,7 @@ module RubyRich
|
|
|
334
334
|
|
|
335
335
|
capture = root.instance_variable_get(:@mouse_capture)
|
|
336
336
|
target = capture && [:mouse_drag, :mouse_up].include?(event_data[:name]) ? capture : (hit_test(event_data[:x], event_data[:y]) || self)
|
|
337
|
+
dispatch_mouse_target_event(event_data, target) if event_data[:name] == :mouse_down
|
|
337
338
|
handled = target.bubble_mouse_event(event_data)
|
|
338
339
|
|
|
339
340
|
root.instance_variable_set(:@mouse_capture, target) if event_data[:name] == :mouse_down && handled
|
|
@@ -341,6 +342,16 @@ module RubyRich
|
|
|
341
342
|
handled
|
|
342
343
|
end
|
|
343
344
|
|
|
345
|
+
def dispatch_mouse_target_event(event_data, target)
|
|
346
|
+
target.bubble_mouse_event(
|
|
347
|
+
event_data.merge(
|
|
348
|
+
name: :mouse_target,
|
|
349
|
+
target_layout: target,
|
|
350
|
+
original_name: event_data[:name]
|
|
351
|
+
)
|
|
352
|
+
)
|
|
353
|
+
end
|
|
354
|
+
|
|
344
355
|
protected
|
|
345
356
|
|
|
346
357
|
def bubble_mouse_event(event_data)
|
|
@@ -15,18 +15,24 @@ module RubyRich
|
|
|
15
15
|
@history_index = nil
|
|
16
16
|
@chars = []
|
|
17
17
|
@cursor = 0
|
|
18
|
+
@value_cache = nil
|
|
19
|
+
@lines_cache = nil
|
|
20
|
+
@line_starts_cache = nil
|
|
21
|
+
@cursor_line_col_cache_key = nil
|
|
22
|
+
@cursor_line_col_cache = nil
|
|
18
23
|
load_history
|
|
19
24
|
history.each { |item| add_history(item, persist: false) }
|
|
20
25
|
end
|
|
21
26
|
|
|
22
27
|
def value
|
|
23
|
-
@chars.join
|
|
28
|
+
@value_cache ||= @chars.join
|
|
24
29
|
end
|
|
25
30
|
|
|
26
31
|
def value=(text)
|
|
27
32
|
@chars = text.to_s.chars
|
|
28
33
|
@cursor = @chars.length
|
|
29
34
|
@history_index = nil
|
|
35
|
+
invalidate_content_cache
|
|
30
36
|
self
|
|
31
37
|
end
|
|
32
38
|
|
|
@@ -38,6 +44,7 @@ module RubyRich
|
|
|
38
44
|
@chars.clear
|
|
39
45
|
@cursor = 0
|
|
40
46
|
@history_index = nil
|
|
47
|
+
invalidate_content_cache
|
|
41
48
|
self
|
|
42
49
|
end
|
|
43
50
|
|
|
@@ -49,6 +56,7 @@ module RubyRich
|
|
|
49
56
|
@chars.insert(@cursor, *new_chars)
|
|
50
57
|
@cursor += new_chars.length
|
|
51
58
|
@history_index = nil
|
|
59
|
+
invalidate_content_cache
|
|
52
60
|
self
|
|
53
61
|
end
|
|
54
62
|
|
|
@@ -62,6 +70,7 @@ module RubyRich
|
|
|
62
70
|
|
|
63
71
|
@chars.delete_at(@cursor - 1)
|
|
64
72
|
@cursor -= 1
|
|
73
|
+
invalidate_content_cache
|
|
65
74
|
true
|
|
66
75
|
end
|
|
67
76
|
|
|
@@ -69,36 +78,43 @@ module RubyRich
|
|
|
69
78
|
return false if @cursor >= @chars.length
|
|
70
79
|
|
|
71
80
|
@chars.delete_at(@cursor)
|
|
81
|
+
invalidate_content_cache
|
|
72
82
|
true
|
|
73
83
|
end
|
|
74
84
|
|
|
75
85
|
def move_left
|
|
76
86
|
@cursor = [@cursor - 1, 0].max
|
|
87
|
+
invalidate_cursor_cache
|
|
77
88
|
self
|
|
78
89
|
end
|
|
79
90
|
|
|
80
91
|
def move_right
|
|
81
92
|
@cursor = [@cursor + 1, @chars.length].min
|
|
93
|
+
invalidate_cursor_cache
|
|
82
94
|
self
|
|
83
95
|
end
|
|
84
96
|
|
|
85
97
|
def home
|
|
86
98
|
@cursor = current_line_start
|
|
99
|
+
invalidate_cursor_cache
|
|
87
100
|
self
|
|
88
101
|
end
|
|
89
102
|
|
|
90
103
|
def end
|
|
91
104
|
@cursor = current_line_end
|
|
105
|
+
invalidate_cursor_cache
|
|
92
106
|
self
|
|
93
107
|
end
|
|
94
108
|
|
|
95
109
|
def buffer_start
|
|
96
110
|
@cursor = 0
|
|
111
|
+
invalidate_cursor_cache
|
|
97
112
|
self
|
|
98
113
|
end
|
|
99
114
|
|
|
100
115
|
def buffer_end
|
|
101
116
|
@cursor = @chars.length
|
|
117
|
+
invalidate_cursor_cache
|
|
102
118
|
self
|
|
103
119
|
end
|
|
104
120
|
|
|
@@ -118,6 +134,7 @@ module RubyRich
|
|
|
118
134
|
return false if @cursor >= @chars.length
|
|
119
135
|
|
|
120
136
|
@chars.slice!(@cursor...current_line_end)
|
|
137
|
+
invalidate_content_cache
|
|
121
138
|
true
|
|
122
139
|
end
|
|
123
140
|
|
|
@@ -127,6 +144,7 @@ module RubyRich
|
|
|
127
144
|
|
|
128
145
|
@chars.slice!(start...@cursor)
|
|
129
146
|
@cursor = start
|
|
147
|
+
invalidate_content_cache
|
|
130
148
|
true
|
|
131
149
|
end
|
|
132
150
|
|
|
@@ -136,6 +154,7 @@ module RubyRich
|
|
|
136
154
|
start = previous_word_start
|
|
137
155
|
@chars.slice!(start...@cursor)
|
|
138
156
|
@cursor = start
|
|
157
|
+
invalidate_content_cache
|
|
139
158
|
true
|
|
140
159
|
end
|
|
141
160
|
|
|
@@ -158,31 +177,63 @@ module RubyRich
|
|
|
158
177
|
end
|
|
159
178
|
|
|
160
179
|
def lines
|
|
161
|
-
|
|
162
|
-
|
|
180
|
+
@lines_cache ||= begin
|
|
181
|
+
text = value
|
|
182
|
+
text.empty? ? [""] : text.split("\n", -1)
|
|
183
|
+
end
|
|
163
184
|
end
|
|
164
185
|
|
|
165
186
|
def cursor_line_col
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
187
|
+
cache_key = [@cursor, @chars.length]
|
|
188
|
+
return @cursor_line_col_cache if @cursor_line_col_cache_key == cache_key && @cursor_line_col_cache
|
|
189
|
+
|
|
190
|
+
line_index = line_index_for_cursor
|
|
191
|
+
@cursor_line_col_cache_key = cache_key
|
|
192
|
+
@cursor_line_col_cache = [line_index, @cursor - line_starts[line_index]]
|
|
169
193
|
end
|
|
170
194
|
|
|
171
195
|
def render_lines(width:, placeholder: nil, focused: true)
|
|
196
|
+
_ = focused
|
|
172
197
|
content = value
|
|
173
198
|
return [placeholder.to_s] if content.empty? && placeholder
|
|
174
199
|
|
|
175
200
|
rendered = []
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
marker_col = focused && index == line_index ? col : nil
|
|
179
|
-
rendered.concat(wrap_line_with_cursor(line, width, marker_col))
|
|
201
|
+
lines.each do |line|
|
|
202
|
+
rendered.concat(wrap_line_without_cursor(line, width))
|
|
180
203
|
end
|
|
181
204
|
rendered.empty? ? [""] : rendered
|
|
182
205
|
end
|
|
183
206
|
|
|
207
|
+
def cursor_visual_position(width:)
|
|
208
|
+
line_index, cursor_col = cursor_line_col
|
|
209
|
+
row = 0
|
|
210
|
+
|
|
211
|
+
lines.each_with_index do |line, index|
|
|
212
|
+
if index == line_index
|
|
213
|
+
cursor_row, display_col = cursor_position_in_wrapped_line(line, width, cursor_col)
|
|
214
|
+
return [row + cursor_row, display_col]
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
row += wrap_line_without_cursor(line, width).length
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
[row, 0]
|
|
221
|
+
end
|
|
222
|
+
|
|
184
223
|
private
|
|
185
224
|
|
|
225
|
+
def invalidate_content_cache
|
|
226
|
+
@value_cache = nil
|
|
227
|
+
@lines_cache = nil
|
|
228
|
+
@line_starts_cache = nil
|
|
229
|
+
invalidate_cursor_cache
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def invalidate_cursor_cache
|
|
233
|
+
@cursor_line_col_cache_key = nil
|
|
234
|
+
@cursor_line_col_cache = nil
|
|
235
|
+
end
|
|
236
|
+
|
|
186
237
|
def normalize_insert_text(text)
|
|
187
238
|
incoming = text.to_s.gsub(/\r\n?/, "\n")
|
|
188
239
|
return incoming if @multiline
|
|
@@ -191,23 +242,13 @@ module RubyRich
|
|
|
191
242
|
end
|
|
192
243
|
|
|
193
244
|
def current_line_start
|
|
194
|
-
|
|
195
|
-
while index >= 0
|
|
196
|
-
return index + 1 if @chars[index] == "\n"
|
|
197
|
-
|
|
198
|
-
index -= 1
|
|
199
|
-
end
|
|
200
|
-
0
|
|
245
|
+
line_starts[line_index_for_cursor]
|
|
201
246
|
end
|
|
202
247
|
|
|
203
248
|
def current_line_end
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
index += 1
|
|
209
|
-
end
|
|
210
|
-
@chars.length
|
|
249
|
+
line_index = line_index_for_cursor
|
|
250
|
+
next_start = line_starts[line_index + 1]
|
|
251
|
+
next_start ? next_start - 1 : @chars.length
|
|
211
252
|
end
|
|
212
253
|
|
|
213
254
|
def move_vertical(delta)
|
|
@@ -216,18 +257,43 @@ module RubyRich
|
|
|
216
257
|
return self if target_line.negative? || target_line >= lines.length
|
|
217
258
|
|
|
218
259
|
@cursor = index_for_line_col(target_line, current_col)
|
|
260
|
+
invalidate_cursor_cache
|
|
219
261
|
self
|
|
220
262
|
end
|
|
221
263
|
|
|
222
264
|
def index_for_line_col(line_index, col)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
line_length = line.chars.length
|
|
226
|
-
return index + [col, line_length].min if current == line_index
|
|
265
|
+
line_start = line_starts[line_index]
|
|
266
|
+
return @chars.length unless line_start
|
|
227
267
|
|
|
228
|
-
|
|
268
|
+
line_length = lines[line_index].chars.length
|
|
269
|
+
line_start + [col, line_length].min
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def line_starts
|
|
273
|
+
@line_starts_cache ||= begin
|
|
274
|
+
starts = [0]
|
|
275
|
+
@chars.each_with_index do |char, index|
|
|
276
|
+
starts << index + 1 if char == "\n"
|
|
277
|
+
end
|
|
278
|
+
starts
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def line_index_for_cursor
|
|
283
|
+
starts = line_starts
|
|
284
|
+
low = 0
|
|
285
|
+
high = starts.length - 1
|
|
286
|
+
|
|
287
|
+
while low <= high
|
|
288
|
+
mid = (low + high) / 2
|
|
289
|
+
if starts[mid] <= @cursor
|
|
290
|
+
low = mid + 1
|
|
291
|
+
else
|
|
292
|
+
high = mid - 1
|
|
293
|
+
end
|
|
229
294
|
end
|
|
230
|
-
|
|
295
|
+
|
|
296
|
+
[high, 0].max
|
|
231
297
|
end
|
|
232
298
|
|
|
233
299
|
def previous_word_start
|
|
@@ -281,41 +347,46 @@ module RubyRich
|
|
|
281
347
|
nil
|
|
282
348
|
end
|
|
283
349
|
|
|
284
|
-
def
|
|
350
|
+
def wrap_line_without_cursor(line, width)
|
|
285
351
|
width = [width, 1].max
|
|
286
352
|
segments = []
|
|
287
353
|
current = +""
|
|
288
354
|
current_width = 0
|
|
289
|
-
chars = line.chars
|
|
290
|
-
chars.each_with_index do |char, index|
|
|
291
|
-
if marker_col == index
|
|
292
|
-
cursor = cursor_marker
|
|
293
|
-
if current_width + 1 > width
|
|
294
|
-
segments << current
|
|
295
|
-
current = +""
|
|
296
|
-
current_width = 0
|
|
297
|
-
end
|
|
298
|
-
current << cursor
|
|
299
|
-
current_width += 1
|
|
300
|
-
end
|
|
301
355
|
|
|
356
|
+
line.chars.each do |char|
|
|
302
357
|
char_width = display_width(char)
|
|
303
|
-
if current_width + char_width > width
|
|
358
|
+
if current_width + char_width > width && !current.empty?
|
|
304
359
|
segments << current
|
|
305
360
|
current = +""
|
|
306
361
|
current_width = 0
|
|
307
362
|
end
|
|
363
|
+
|
|
308
364
|
current << char
|
|
309
365
|
current_width += char_width
|
|
310
366
|
end
|
|
311
367
|
|
|
312
|
-
current << cursor_marker if marker_col == chars.length
|
|
313
368
|
segments << current
|
|
314
369
|
segments
|
|
315
370
|
end
|
|
316
371
|
|
|
317
|
-
def
|
|
318
|
-
|
|
372
|
+
def cursor_position_in_wrapped_line(line, width, cursor_col)
|
|
373
|
+
width = [width, 1].max
|
|
374
|
+
row = 0
|
|
375
|
+
display_col = 0
|
|
376
|
+
|
|
377
|
+
line.chars.each_with_index do |char, index|
|
|
378
|
+
char_width = display_width(char)
|
|
379
|
+
if display_col + char_width > width && display_col.positive?
|
|
380
|
+
row += 1
|
|
381
|
+
display_col = 0
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
return [row, display_col] if index == cursor_col
|
|
385
|
+
|
|
386
|
+
display_col += char_width
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
[row, display_col]
|
|
319
390
|
end
|
|
320
391
|
|
|
321
392
|
def display_width(char)
|
data/lib/ruby_rich/live.rb
CHANGED
|
@@ -12,9 +12,9 @@ module RubyRich
|
|
|
12
12
|
@cache = nil
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def print_with_pos(x,y,
|
|
15
|
+
def print_with_pos(x,y,text)
|
|
16
16
|
print "\e[#{y};#{x}H" # Move cursor to top-left
|
|
17
|
-
print
|
|
17
|
+
print text
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def draw(buffer)
|
|
@@ -23,19 +23,37 @@ module RubyRich
|
|
|
23
23
|
draw_full(buffer)
|
|
24
24
|
@cache = buffer
|
|
25
25
|
else
|
|
26
|
-
buffer
|
|
27
|
-
arr.each_with_index do |char, x|
|
|
28
|
-
if @cache[y][x] != char
|
|
29
|
-
print_with_pos(x + 1 , y + 1 , char)
|
|
30
|
-
@cache[y][x] = char
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
26
|
+
draw_changes(buffer)
|
|
34
27
|
end
|
|
35
28
|
end
|
|
36
29
|
|
|
37
30
|
private
|
|
38
31
|
|
|
32
|
+
def draw_changes(buffer)
|
|
33
|
+
buffer.each_with_index do |line, y|
|
|
34
|
+
cache_line = @cache[y] ||= []
|
|
35
|
+
x = 0
|
|
36
|
+
while x < line.length
|
|
37
|
+
if cache_line[x] == line[x]
|
|
38
|
+
x += 1
|
|
39
|
+
next
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
start = x
|
|
43
|
+
parts = []
|
|
44
|
+
while x < line.length && cache_line[x] != line[x]
|
|
45
|
+
char = line[x]
|
|
46
|
+
parts << char unless char.nil?
|
|
47
|
+
cache_line[x] = char
|
|
48
|
+
x += 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
print_with_pos(start + 1, y + 1, parts.join) unless parts.empty?
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
$stdout.flush
|
|
55
|
+
end
|
|
56
|
+
|
|
39
57
|
def draw_full(buffer)
|
|
40
58
|
buffer.each_with_index do |line, y|
|
|
41
59
|
print_with_pos(1, y + 1, line.compact.join(""))
|
|
@@ -45,6 +63,8 @@ module RubyRich
|
|
|
45
63
|
end
|
|
46
64
|
|
|
47
65
|
class Live
|
|
66
|
+
RESIZE_POLL_INTERVAL = 0.25
|
|
67
|
+
|
|
48
68
|
attr_accessor :params, :app, :listening, :layout
|
|
49
69
|
class << self
|
|
50
70
|
def start(layout, refresh_rate: 30, mouse: false, alt_screen: false, autowrap: false, &proc)
|
|
@@ -87,6 +107,10 @@ module RubyRich
|
|
|
87
107
|
@event_queue = Queue.new
|
|
88
108
|
@action_queue = Queue.new
|
|
89
109
|
@event_thread = nil
|
|
110
|
+
@wake_mutex = Mutex.new
|
|
111
|
+
@wake_condition = ConditionVariable.new
|
|
112
|
+
@dirty = true
|
|
113
|
+
@last_terminal_size = nil
|
|
90
114
|
@params = {}
|
|
91
115
|
if (log_path = ENV["RUBY_RICH_LOG"]).to_s.strip != ""
|
|
92
116
|
FileUtils.mkdir_p(File.dirname(log_path))
|
|
@@ -97,12 +121,15 @@ module RubyRich
|
|
|
97
121
|
def run(proc = nil)
|
|
98
122
|
start_event_thread if @listening
|
|
99
123
|
while @running
|
|
100
|
-
drain_action_queue
|
|
124
|
+
action_processed = drain_action_queue
|
|
101
125
|
break unless @running
|
|
102
126
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
127
|
+
event_processed = @listening ? drain_event_queue : false
|
|
128
|
+
if consume_dirty || action_processed || event_processed || terminal_size_changed?
|
|
129
|
+
render_frame
|
|
130
|
+
else
|
|
131
|
+
wait_for_activity
|
|
132
|
+
end
|
|
106
133
|
end
|
|
107
134
|
rescue Interrupt
|
|
108
135
|
@running = false
|
|
@@ -113,11 +140,21 @@ module RubyRich
|
|
|
113
140
|
return false unless @running
|
|
114
141
|
|
|
115
142
|
@action_queue << block
|
|
143
|
+
wake
|
|
144
|
+
true
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def refresh
|
|
148
|
+
return false unless @running
|
|
149
|
+
|
|
150
|
+
mark_dirty
|
|
151
|
+
wake
|
|
116
152
|
true
|
|
117
153
|
end
|
|
118
154
|
|
|
119
155
|
def stop
|
|
120
156
|
@running = false
|
|
157
|
+
wake
|
|
121
158
|
shutdown
|
|
122
159
|
RubyRich::Terminal.clear
|
|
123
160
|
end
|
|
@@ -150,7 +187,10 @@ module RubyRich
|
|
|
150
187
|
while @running
|
|
151
188
|
begin
|
|
152
189
|
event_data = @console.get_event
|
|
153
|
-
|
|
190
|
+
if event_data
|
|
191
|
+
@event_queue << event_data
|
|
192
|
+
wake
|
|
193
|
+
end
|
|
154
194
|
rescue IOError, SystemCallError
|
|
155
195
|
break
|
|
156
196
|
rescue Interrupt
|
|
@@ -164,28 +204,90 @@ module RubyRich
|
|
|
164
204
|
end
|
|
165
205
|
|
|
166
206
|
def drain_event_queue
|
|
207
|
+
processed = false
|
|
167
208
|
until @event_queue.empty?
|
|
168
209
|
event_data = @event_queue.pop(true)
|
|
169
210
|
@layout.notify_listeners(event_data)
|
|
211
|
+
processed = true
|
|
170
212
|
end
|
|
213
|
+
processed
|
|
171
214
|
rescue ThreadError
|
|
172
|
-
|
|
215
|
+
processed
|
|
173
216
|
end
|
|
174
217
|
|
|
175
218
|
def drain_action_queue
|
|
219
|
+
processed = false
|
|
176
220
|
until @action_queue.empty?
|
|
177
221
|
action = @action_queue.pop(true)
|
|
178
222
|
action.call(self)
|
|
223
|
+
processed = true
|
|
179
224
|
end
|
|
225
|
+
processed
|
|
180
226
|
rescue ThreadError
|
|
181
|
-
|
|
227
|
+
processed
|
|
182
228
|
rescue => e
|
|
183
229
|
RubyRich.logger.error("UI action failed: #{e.class}: #{e.message}")
|
|
230
|
+
true
|
|
184
231
|
end
|
|
185
232
|
|
|
186
233
|
def render_frame
|
|
187
|
-
|
|
234
|
+
width = terminal_width
|
|
235
|
+
height = terminal_height
|
|
236
|
+
@last_terminal_size = [width, height]
|
|
237
|
+
@layout.calculate_dimensions(width, height)
|
|
188
238
|
@render.draw(@layout.render_to_buffer)
|
|
239
|
+
position_native_cursor
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def position_native_cursor
|
|
243
|
+
composer_layout = @layout[:composer]
|
|
244
|
+
return unless composer_layout
|
|
245
|
+
|
|
246
|
+
frame = composer_layout.content
|
|
247
|
+
component = frame.instance_variable_get(:@component) if frame
|
|
248
|
+
return unless component&.respond_to?(:native_cursor_position)
|
|
249
|
+
|
|
250
|
+
cursor = component.native_cursor_position
|
|
251
|
+
return unless cursor
|
|
252
|
+
|
|
253
|
+
row, col = cursor
|
|
254
|
+
terminal_row = composer_layout.y_offset.to_i + 1 + row.to_i
|
|
255
|
+
terminal_col = composer_layout.x_offset.to_i + 1 + col.to_i
|
|
256
|
+
print "\e[#{terminal_row + 1};#{terminal_col + 1}H"
|
|
257
|
+
$stdout.flush
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def wait_for_activity
|
|
261
|
+
@wake_mutex.synchronize do
|
|
262
|
+
return unless @running
|
|
263
|
+
return unless @action_queue.empty?
|
|
264
|
+
return if @listening && !@event_queue.empty?
|
|
265
|
+
|
|
266
|
+
@wake_condition.wait(@wake_mutex, RESIZE_POLL_INTERVAL)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def wake
|
|
271
|
+
@wake_mutex.synchronize { @wake_condition.signal }
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def mark_dirty
|
|
275
|
+
@wake_mutex.synchronize { @dirty = true }
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def consume_dirty
|
|
279
|
+
@wake_mutex.synchronize do
|
|
280
|
+
dirty = @dirty
|
|
281
|
+
@dirty = false
|
|
282
|
+
dirty
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def terminal_size_changed?
|
|
287
|
+
current_size = [terminal_width, terminal_height]
|
|
288
|
+
changed = @last_terminal_size != current_size
|
|
289
|
+
@last_terminal_size = current_size
|
|
290
|
+
changed
|
|
189
291
|
end
|
|
190
292
|
|
|
191
293
|
def terminal_width
|
data/lib/ruby_rich/panel.rb
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
module RubyRich
|
|
2
2
|
class Panel
|
|
3
|
-
attr_accessor :
|
|
3
|
+
attr_accessor :height, :content, :line_pos, :border_style, :title
|
|
4
4
|
attr_accessor :title_align, :content_changed
|
|
5
|
+
attr_reader :width
|
|
5
6
|
|
|
6
7
|
def initialize(content = "", title: nil, border_style: :white, title_align: :center)
|
|
7
8
|
@content = content
|
|
@@ -11,6 +12,14 @@ module RubyRich
|
|
|
11
12
|
@height = 0
|
|
12
13
|
@line_pos = 0
|
|
13
14
|
@title_align = title_align
|
|
15
|
+
@wrapped_content_cache_key = nil
|
|
16
|
+
@wrapped_content_cache = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def width=(new_width)
|
|
20
|
+
new_width = new_width.to_i
|
|
21
|
+
invalidate_wrap_cache if @width != new_width
|
|
22
|
+
@width = new_width
|
|
14
23
|
end
|
|
15
24
|
|
|
16
25
|
def inner_width
|
|
@@ -117,6 +126,7 @@ module RubyRich
|
|
|
117
126
|
|
|
118
127
|
def content=(new_content)
|
|
119
128
|
@content = new_content
|
|
129
|
+
invalidate_wrap_cache
|
|
120
130
|
content_lines = wrap_content(@content)
|
|
121
131
|
if content_lines.size > @height - 2
|
|
122
132
|
@line_pos = content_lines.size - @height + 2
|
|
@@ -126,6 +136,11 @@ module RubyRich
|
|
|
126
136
|
|
|
127
137
|
private
|
|
128
138
|
|
|
139
|
+
def invalidate_wrap_cache
|
|
140
|
+
@wrapped_content_cache_key = nil
|
|
141
|
+
@wrapped_content_cache = nil
|
|
142
|
+
end
|
|
143
|
+
|
|
129
144
|
def split_text_by_width(text)
|
|
130
145
|
result = []
|
|
131
146
|
current_line = ""
|
|
@@ -165,9 +180,13 @@ module RubyRich
|
|
|
165
180
|
end
|
|
166
181
|
|
|
167
182
|
def wrap_content(text)
|
|
183
|
+
key = [text, @width]
|
|
184
|
+
return @wrapped_content_cache if @wrapped_content_cache_key == key && @wrapped_content_cache
|
|
185
|
+
|
|
186
|
+
@wrapped_content_cache_key = key
|
|
168
187
|
text.split("\n").flat_map do |line|
|
|
169
188
|
split_text_by_width(line)
|
|
170
|
-
end
|
|
189
|
+
end.tap { |lines| @wrapped_content_cache = lines }
|
|
171
190
|
end
|
|
172
191
|
end
|
|
173
192
|
end
|
data/lib/ruby_rich/table.rb
CHANGED
|
@@ -126,7 +126,8 @@ module RubyRich
|
|
|
126
126
|
border_chars = BORDER_STYLES[@border_style]
|
|
127
127
|
|
|
128
128
|
row_content = row.map.with_index do |cell, i|
|
|
129
|
-
|
|
129
|
+
rendered = cell.render
|
|
130
|
+
content = bold ? rendered : align_cell(rendered, column_widths[i])
|
|
130
131
|
aligned_content = align_cell(content, column_widths[i])
|
|
131
132
|
" #{aligned_content} "
|
|
132
133
|
end.join(border_chars[:vertical])
|
|
@@ -143,12 +144,13 @@ module RubyRich
|
|
|
143
144
|
|
|
144
145
|
# Prepare each cell's lines
|
|
145
146
|
row_lines = row.map.with_index do |cell, i|
|
|
147
|
+
rendered = cell.render
|
|
146
148
|
# 获取单元格的样式序列
|
|
147
|
-
style_sequence =
|
|
149
|
+
style_sequence = rendered.match(/\e\[[0-9;]*m/)&.to_s || ""
|
|
148
150
|
reset_sequence = style_sequence.empty? ? "" : "\e[0m"
|
|
149
151
|
|
|
150
152
|
# 分割成多行并保持样式
|
|
151
|
-
cell_content =
|
|
153
|
+
cell_content = rendered.split("\n")
|
|
152
154
|
|
|
153
155
|
# 为每一行添加样式
|
|
154
156
|
cell_content.map! { |line|
|
|
@@ -165,8 +167,8 @@ module RubyRich
|
|
|
165
167
|
|
|
166
168
|
# Normalize row height
|
|
167
169
|
max_height = row_lines.map(&:size).max
|
|
168
|
-
row_lines.
|
|
169
|
-
width = column_widths[
|
|
170
|
+
row_lines.each_with_index do |lines, index|
|
|
171
|
+
width = column_widths[index]
|
|
170
172
|
style_sequence = lines.first.match(/\e\[[0-9;]*m/)&.to_s || ""
|
|
171
173
|
reset_sequence = style_sequence.empty? ? "" : "\e[0m"
|
|
172
174
|
lines.fill(style_sequence + " " * width + reset_sequence, lines.size...max_height)
|
|
@@ -188,7 +190,7 @@ module RubyRich
|
|
|
188
190
|
|
|
189
191
|
# Calculate widths from headers
|
|
190
192
|
@headers.each_with_index do |header, i|
|
|
191
|
-
header_text = header
|
|
193
|
+
header_text = render_cell(header)
|
|
192
194
|
header_width = Unicode::DisplayWidth.of(header_text.gsub(/\e\[[0-9;]*m/, ''))
|
|
193
195
|
widths[i] = [widths[i], header_width].max
|
|
194
196
|
end
|
|
@@ -196,7 +198,7 @@ module RubyRich
|
|
|
196
198
|
# Calculate widths from rows
|
|
197
199
|
@rows.each do |row|
|
|
198
200
|
row.each_with_index do |cell, i|
|
|
199
|
-
cell_lines = cell.
|
|
201
|
+
cell_lines = render_cell(cell).split("\n")
|
|
200
202
|
cell_lines.each do |line|
|
|
201
203
|
# Remove ANSI escape sequences before calculating width
|
|
202
204
|
plain_line = line.gsub(/\e\[[0-9;]*m/, '')
|
|
@@ -211,7 +213,8 @@ module RubyRich
|
|
|
211
213
|
|
|
212
214
|
def render_row(row, column_widths, bold: false)
|
|
213
215
|
row.map.with_index do |cell, i|
|
|
214
|
-
|
|
216
|
+
rendered = cell.render
|
|
217
|
+
content = bold ? rendered : align_cell(rendered, column_widths[i])
|
|
215
218
|
align_cell(content, column_widths[i])
|
|
216
219
|
end.join(" | ").prepend("| ").concat(" |")
|
|
217
220
|
end
|
|
@@ -219,12 +222,13 @@ module RubyRich
|
|
|
219
222
|
def render_multiline_row(row, column_widths)
|
|
220
223
|
# Prepare each cell's lines
|
|
221
224
|
row_lines = row.map.with_index do |cell, i|
|
|
225
|
+
rendered = cell.render
|
|
222
226
|
# Get cell style sequence
|
|
223
|
-
style_sequence =
|
|
227
|
+
style_sequence = rendered.match(/\e\[[0-9;]*m/)&.to_s || ""
|
|
224
228
|
reset_sequence = style_sequence.empty? ? "" : "\e[0m"
|
|
225
229
|
|
|
226
230
|
# Split into multiple lines while preserving styles
|
|
227
|
-
cell_content =
|
|
231
|
+
cell_content = rendered.split("\n")
|
|
228
232
|
|
|
229
233
|
# Add style to each line
|
|
230
234
|
cell_content.map! { |line|
|
|
@@ -241,8 +245,8 @@ module RubyRich
|
|
|
241
245
|
|
|
242
246
|
# Normalize row height
|
|
243
247
|
max_height = row_lines.map(&:size).max
|
|
244
|
-
row_lines.
|
|
245
|
-
width = column_widths[
|
|
248
|
+
row_lines.each_with_index do |lines, index|
|
|
249
|
+
width = column_widths[index]
|
|
246
250
|
style_sequence = lines.first.match(/\e\[[0-9;]*m/)&.to_s || ""
|
|
247
251
|
reset_sequence = style_sequence.empty? ? "" : "\e[0m"
|
|
248
252
|
lines.fill(style_sequence + " " * width + reset_sequence, lines.size...max_height)
|
|
@@ -253,6 +257,10 @@ module RubyRich
|
|
|
253
257
|
row_lines.map { |lines| lines[line_index] }.join(" | ").prepend("| ").concat(" |")
|
|
254
258
|
end
|
|
255
259
|
end
|
|
260
|
+
|
|
261
|
+
def render_cell(cell)
|
|
262
|
+
cell.respond_to?(:render) ? cell.render : cell.to_s
|
|
263
|
+
end
|
|
256
264
|
|
|
257
265
|
def align_cell(content, width)
|
|
258
266
|
style_sequences = content.scan(/\e\[[0-9;]*m/)
|
|
@@ -280,4 +288,4 @@ module RubyRich
|
|
|
280
288
|
end
|
|
281
289
|
end
|
|
282
290
|
end
|
|
283
|
-
end
|
|
291
|
+
end
|
data/lib/ruby_rich/terminal.rb
CHANGED
|
@@ -42,7 +42,7 @@ module RubyRich
|
|
|
42
42
|
class << self
|
|
43
43
|
attr_accessor :debug_input
|
|
44
44
|
|
|
45
|
-
def setup(mouse: false, hide_cursor:
|
|
45
|
+
def setup(mouse: false, hide_cursor: false, autowrap: true, alt_screen: false)
|
|
46
46
|
capture_state
|
|
47
47
|
enable_virtual_terminal_on_windows
|
|
48
48
|
system('stty -echo') unless windows?
|
data/lib/ruby_rich/transcript.rb
CHANGED
|
@@ -160,11 +160,12 @@ module RubyRich
|
|
|
160
160
|
class Store
|
|
161
161
|
include Enumerable
|
|
162
162
|
|
|
163
|
-
attr_reader :entries
|
|
163
|
+
attr_reader :entries, :version
|
|
164
164
|
|
|
165
165
|
def initialize
|
|
166
166
|
@entries = []
|
|
167
167
|
@sequence = 0
|
|
168
|
+
@version = 0
|
|
168
169
|
@mutex = Mutex.new
|
|
169
170
|
end
|
|
170
171
|
|
|
@@ -183,6 +184,7 @@ module RubyRich
|
|
|
183
184
|
name: name
|
|
184
185
|
)
|
|
185
186
|
@entries << entry
|
|
187
|
+
touch
|
|
186
188
|
entry
|
|
187
189
|
end
|
|
188
190
|
end
|
|
@@ -201,6 +203,7 @@ module RubyRich
|
|
|
201
203
|
return false unless index
|
|
202
204
|
|
|
203
205
|
@entries.delete_at(index)
|
|
206
|
+
touch
|
|
204
207
|
true
|
|
205
208
|
end
|
|
206
209
|
end
|
|
@@ -241,10 +244,15 @@ module RubyRich
|
|
|
241
244
|
return false unless entry
|
|
242
245
|
|
|
243
246
|
yield entry
|
|
247
|
+
touch
|
|
244
248
|
true
|
|
245
249
|
end
|
|
246
250
|
end
|
|
247
251
|
|
|
252
|
+
def touch
|
|
253
|
+
@version += 1
|
|
254
|
+
end
|
|
255
|
+
|
|
248
256
|
def normalize_type(type)
|
|
249
257
|
normalized = type.to_sym
|
|
250
258
|
normalized = :tool if normalized == :tool_call
|
|
@@ -278,6 +286,10 @@ module RubyRich
|
|
|
278
286
|
@store.entries
|
|
279
287
|
end
|
|
280
288
|
|
|
289
|
+
def version
|
|
290
|
+
@store.version
|
|
291
|
+
end
|
|
292
|
+
|
|
281
293
|
def focus
|
|
282
294
|
@focused = true
|
|
283
295
|
self
|
data/lib/ruby_rich/version.rb
CHANGED
data/lib/ruby_rich/viewport.rb
CHANGED
|
@@ -14,6 +14,7 @@ module RubyRich
|
|
|
14
14
|
@height = 0
|
|
15
15
|
@scroll_top = 0
|
|
16
16
|
@dragging_scrollbar = false
|
|
17
|
+
@dragging_viewport = false
|
|
17
18
|
@drag_start_y = nil
|
|
18
19
|
@drag_start_scroll_top = nil
|
|
19
20
|
@selecting = false
|
|
@@ -21,6 +22,8 @@ module RubyRich
|
|
|
21
22
|
@selection_end = nil
|
|
22
23
|
@selected_text = ""
|
|
23
24
|
@focused = true
|
|
25
|
+
@rendered_lines_cache_key = nil
|
|
26
|
+
@rendered_lines_cache = nil
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
def focus
|
|
@@ -36,6 +39,7 @@ module RubyRich
|
|
|
36
39
|
def content=(new_content)
|
|
37
40
|
was_at_bottom = at_bottom?
|
|
38
41
|
@content = new_content
|
|
42
|
+
invalidate_rendered_lines
|
|
39
43
|
scroll_to_bottom if @auto_scroll && was_at_bottom
|
|
40
44
|
clamp_scroll
|
|
41
45
|
end
|
|
@@ -59,7 +63,6 @@ module RubyRich
|
|
|
59
63
|
|
|
60
64
|
def handle_event(event_data, layout = nil)
|
|
61
65
|
return false if keyboard_event?(event_data) && !@focused
|
|
62
|
-
return false if mouse_event?(event_data) && !@focused
|
|
63
66
|
|
|
64
67
|
case event_data[:name]
|
|
65
68
|
when :page_up
|
|
@@ -86,11 +89,11 @@ module RubyRich
|
|
|
86
89
|
when :mouse_down
|
|
87
90
|
return copy_selection if event_data[:button] == :right
|
|
88
91
|
|
|
89
|
-
start_scrollbar_drag(event_data, layout) ||
|
|
92
|
+
start_scrollbar_drag(event_data, layout) || start_viewport_drag(event_data, layout)
|
|
90
93
|
when :mouse_drag
|
|
91
|
-
drag_scrollbar(event_data, layout) || drag_selection(event_data, layout)
|
|
94
|
+
drag_scrollbar(event_data, layout) || drag_viewport(event_data, layout) || drag_selection(event_data, layout)
|
|
92
95
|
when :mouse_up
|
|
93
|
-
stop_scrollbar_drag || stop_selection
|
|
96
|
+
stop_scrollbar_drag || stop_viewport_drag || stop_selection
|
|
94
97
|
else
|
|
95
98
|
false
|
|
96
99
|
end
|
|
@@ -140,7 +143,24 @@ module RubyRich
|
|
|
140
143
|
private
|
|
141
144
|
|
|
142
145
|
def rendered_lines
|
|
143
|
-
|
|
146
|
+
key = rendered_lines_cache_key(@content)
|
|
147
|
+
return @rendered_lines_cache if @rendered_lines_cache_key == key && @rendered_lines_cache
|
|
148
|
+
|
|
149
|
+
@rendered_lines_cache_key = key
|
|
150
|
+
@rendered_lines_cache = normalize_lines(@content)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def invalidate_rendered_lines
|
|
154
|
+
@rendered_lines_cache_key = nil
|
|
155
|
+
@rendered_lines_cache = nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def rendered_lines_cache_key(value)
|
|
159
|
+
[
|
|
160
|
+
value.object_id,
|
|
161
|
+
content_width,
|
|
162
|
+
value.respond_to?(:version) ? value.version : nil
|
|
163
|
+
]
|
|
144
164
|
end
|
|
145
165
|
|
|
146
166
|
def keyboard_event?(event_data)
|
|
@@ -162,12 +182,58 @@ module RubyRich
|
|
|
162
182
|
|
|
163
183
|
case rendered
|
|
164
184
|
when String
|
|
165
|
-
rendered.split("\n")
|
|
185
|
+
rendered.split("\n").flat_map { |line| wrap_display_line(line, content_width) }
|
|
166
186
|
when Array
|
|
167
|
-
rendered
|
|
187
|
+
rendered.flat_map { |line| wrap_display_line(line.to_s, content_width) }
|
|
168
188
|
else
|
|
169
|
-
rendered.to_s.split("\n")
|
|
189
|
+
rendered.to_s.split("\n").flat_map { |line| wrap_display_line(line, content_width) }
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def wrap_display_line(line, target_width)
|
|
194
|
+
return [""] if line.empty?
|
|
195
|
+
return [line] if display_width(line) <= target_width
|
|
196
|
+
|
|
197
|
+
lines = []
|
|
198
|
+
current = +""
|
|
199
|
+
width = 0
|
|
200
|
+
in_escape = false
|
|
201
|
+
escape = +""
|
|
202
|
+
active_sgr = +""
|
|
203
|
+
|
|
204
|
+
line.each_char do |char|
|
|
205
|
+
if in_escape
|
|
206
|
+
escape << char
|
|
207
|
+
if char == "m"
|
|
208
|
+
current << escape
|
|
209
|
+
active_sgr = escape == AnsiCode.reset ? +"" : escape.dup
|
|
210
|
+
escape = +""
|
|
211
|
+
in_escape = false
|
|
212
|
+
end
|
|
213
|
+
next
|
|
214
|
+
elsif char.ord == 27
|
|
215
|
+
escape << char
|
|
216
|
+
in_escape = true
|
|
217
|
+
next
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
char_width = Unicode::DisplayWidth.of(char)
|
|
221
|
+
if width.positive? && width + char_width > target_width
|
|
222
|
+
lines << close_wrapped_line(current, active_sgr)
|
|
223
|
+
current = active_sgr.dup
|
|
224
|
+
width = 0
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
current << char
|
|
228
|
+
width += char_width
|
|
170
229
|
end
|
|
230
|
+
|
|
231
|
+
lines << current unless current.empty?
|
|
232
|
+
lines.empty? ? [""] : lines
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def close_wrapped_line(line, active_sgr)
|
|
236
|
+
active_sgr.empty? || line.end_with?(AnsiCode.reset) ? line : "#{line}#{AnsiCode.reset}"
|
|
171
237
|
end
|
|
172
238
|
|
|
173
239
|
def page_size
|
|
@@ -286,6 +352,18 @@ module RubyRich
|
|
|
286
352
|
true
|
|
287
353
|
end
|
|
288
354
|
|
|
355
|
+
def start_viewport_drag(event_data, layout)
|
|
356
|
+
return false unless layout
|
|
357
|
+
return false unless event_data[:button] == :left
|
|
358
|
+
return false if event_data[:x] >= layout.x_offset + content_width
|
|
359
|
+
return false unless event_data[:y].between?(layout.y_offset, layout.y_offset + @height - 1)
|
|
360
|
+
|
|
361
|
+
@dragging_viewport = true
|
|
362
|
+
@drag_start_y = event_data[:y]
|
|
363
|
+
@drag_start_scroll_top = @scroll_top
|
|
364
|
+
true
|
|
365
|
+
end
|
|
366
|
+
|
|
289
367
|
def drag_scrollbar(event_data, layout)
|
|
290
368
|
return false unless @dragging_scrollbar && layout
|
|
291
369
|
|
|
@@ -301,6 +379,14 @@ module RubyRich
|
|
|
301
379
|
true
|
|
302
380
|
end
|
|
303
381
|
|
|
382
|
+
def drag_viewport(event_data, layout)
|
|
383
|
+
return false unless @dragging_viewport && layout
|
|
384
|
+
|
|
385
|
+
delta_y = event_data[:y] - @drag_start_y
|
|
386
|
+
scroll_to(@drag_start_scroll_top - delta_y)
|
|
387
|
+
true
|
|
388
|
+
end
|
|
389
|
+
|
|
304
390
|
def drag_selection(event_data, layout)
|
|
305
391
|
return false unless @selecting && layout
|
|
306
392
|
|
|
@@ -316,6 +402,14 @@ module RubyRich
|
|
|
316
402
|
was_dragging
|
|
317
403
|
end
|
|
318
404
|
|
|
405
|
+
def stop_viewport_drag
|
|
406
|
+
was_dragging = @dragging_viewport
|
|
407
|
+
@dragging_viewport = false
|
|
408
|
+
@drag_start_y = nil
|
|
409
|
+
@drag_start_scroll_top = nil
|
|
410
|
+
was_dragging
|
|
411
|
+
end
|
|
412
|
+
|
|
319
413
|
def stop_selection
|
|
320
414
|
return false unless @selecting
|
|
321
415
|
|