ruby_rich 0.4.3 → 0.4.4
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/app_shell.rb +8 -2
- data/lib/ruby_rich/composer.rb +2 -0
- data/lib/ruby_rich/line_editor.rb +81 -41
- data/lib/ruby_rich/live.rb +101 -18
- data/lib/ruby_rich/panel.rb +21 -2
- data/lib/ruby_rich/table.rb +21 -13
- data/lib/ruby_rich/transcript.rb +13 -1
- data/lib/ruby_rich/version.rb +1 -1
- data/lib/ruby_rich/viewport.rb +70 -4
- 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: 50bb2ebd116fd55abbe618cbd351ad1b73dab3a040d5df2f43011aff77a608bf
|
|
4
|
+
data.tar.gz: be41ae4ba9c0d4754c3eb9de3070d914e6dba23dd383f5fc81ee72c848f8e4a6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c71f39b536218b86f9ce58e15ab476d7927bc086be78ec46f36629e9d3007972f5b4c727a924fad3729666362ae3fb600445ff0acd265e58bac9d266097b4edf
|
|
7
|
+
data.tar.gz: 2424396618c67fdad4568d77917424377c70e0284572ceb8e2735a53ccf9d10d217f1273cbc337ffd9063b70c9cfe60b310deb34278b619232ef8302140d8dc4
|
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
|
data/lib/ruby_rich/composer.rb
CHANGED
|
@@ -433,6 +433,8 @@ module RubyRich
|
|
|
433
433
|
def colorize_input(line)
|
|
434
434
|
color = AnsiCode.color(:white, true)
|
|
435
435
|
reset = AnsiCode.reset
|
|
436
|
+
return line.to_s if line.to_s.include?(AnsiCode.inverse)
|
|
437
|
+
|
|
436
438
|
"#{color}#{line.to_s.gsub(reset, "#{reset}#{color}")}#{reset}"
|
|
437
439
|
end
|
|
438
440
|
|
|
@@ -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,14 +177,19 @@ 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)
|
|
@@ -183,6 +207,18 @@ module RubyRich
|
|
|
183
207
|
|
|
184
208
|
private
|
|
185
209
|
|
|
210
|
+
def invalidate_content_cache
|
|
211
|
+
@value_cache = nil
|
|
212
|
+
@lines_cache = nil
|
|
213
|
+
@line_starts_cache = nil
|
|
214
|
+
invalidate_cursor_cache
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def invalidate_cursor_cache
|
|
218
|
+
@cursor_line_col_cache_key = nil
|
|
219
|
+
@cursor_line_col_cache = nil
|
|
220
|
+
end
|
|
221
|
+
|
|
186
222
|
def normalize_insert_text(text)
|
|
187
223
|
incoming = text.to_s.gsub(/\r\n?/, "\n")
|
|
188
224
|
return incoming if @multiline
|
|
@@ -191,23 +227,13 @@ module RubyRich
|
|
|
191
227
|
end
|
|
192
228
|
|
|
193
229
|
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
|
|
230
|
+
line_starts[line_index_for_cursor]
|
|
201
231
|
end
|
|
202
232
|
|
|
203
233
|
def current_line_end
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
index += 1
|
|
209
|
-
end
|
|
210
|
-
@chars.length
|
|
234
|
+
line_index = line_index_for_cursor
|
|
235
|
+
next_start = line_starts[line_index + 1]
|
|
236
|
+
next_start ? next_start - 1 : @chars.length
|
|
211
237
|
end
|
|
212
238
|
|
|
213
239
|
def move_vertical(delta)
|
|
@@ -216,18 +242,43 @@ module RubyRich
|
|
|
216
242
|
return self if target_line.negative? || target_line >= lines.length
|
|
217
243
|
|
|
218
244
|
@cursor = index_for_line_col(target_line, current_col)
|
|
245
|
+
invalidate_cursor_cache
|
|
219
246
|
self
|
|
220
247
|
end
|
|
221
248
|
|
|
222
249
|
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
|
|
250
|
+
line_start = line_starts[line_index]
|
|
251
|
+
return @chars.length unless line_start
|
|
227
252
|
|
|
228
|
-
|
|
253
|
+
line_length = lines[line_index].chars.length
|
|
254
|
+
line_start + [col, line_length].min
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def line_starts
|
|
258
|
+
@line_starts_cache ||= begin
|
|
259
|
+
starts = [0]
|
|
260
|
+
@chars.each_with_index do |char, index|
|
|
261
|
+
starts << index + 1 if char == "\n"
|
|
262
|
+
end
|
|
263
|
+
starts
|
|
229
264
|
end
|
|
230
|
-
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def line_index_for_cursor
|
|
268
|
+
starts = line_starts
|
|
269
|
+
low = 0
|
|
270
|
+
high = starts.length - 1
|
|
271
|
+
|
|
272
|
+
while low <= high
|
|
273
|
+
mid = (low + high) / 2
|
|
274
|
+
if starts[mid] <= @cursor
|
|
275
|
+
low = mid + 1
|
|
276
|
+
else
|
|
277
|
+
high = mid - 1
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
[high, 0].max
|
|
231
282
|
end
|
|
232
283
|
|
|
233
284
|
def previous_word_start
|
|
@@ -288,34 +339,23 @@ module RubyRich
|
|
|
288
339
|
current_width = 0
|
|
289
340
|
chars = line.chars
|
|
290
341
|
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
|
-
|
|
302
342
|
char_width = display_width(char)
|
|
303
343
|
if current_width + char_width > width
|
|
304
344
|
segments << current
|
|
305
345
|
current = +""
|
|
306
346
|
current_width = 0
|
|
307
347
|
end
|
|
308
|
-
current << char
|
|
348
|
+
current << (marker_col == index ? cursor_marker(char) : char)
|
|
309
349
|
current_width += char_width
|
|
310
350
|
end
|
|
311
351
|
|
|
312
|
-
current << cursor_marker if marker_col == chars.length
|
|
352
|
+
current << cursor_marker(" ") if marker_col == chars.length
|
|
313
353
|
segments << current
|
|
314
354
|
segments
|
|
315
355
|
end
|
|
316
356
|
|
|
317
|
-
def cursor_marker
|
|
318
|
-
"#{AnsiCode.
|
|
357
|
+
def cursor_marker(char)
|
|
358
|
+
"#{AnsiCode.inverse}#{char}#{AnsiCode.reset}"
|
|
319
359
|
end
|
|
320
360
|
|
|
321
361
|
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,30 +204,73 @@ 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)
|
|
189
239
|
end
|
|
190
240
|
|
|
241
|
+
def wait_for_activity
|
|
242
|
+
@wake_mutex.synchronize do
|
|
243
|
+
return unless @running
|
|
244
|
+
return unless @action_queue.empty?
|
|
245
|
+
return if @listening && !@event_queue.empty?
|
|
246
|
+
|
|
247
|
+
@wake_condition.wait(@wake_mutex, RESIZE_POLL_INTERVAL)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def wake
|
|
252
|
+
@wake_mutex.synchronize { @wake_condition.signal }
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def mark_dirty
|
|
256
|
+
@wake_mutex.synchronize { @dirty = true }
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def consume_dirty
|
|
260
|
+
@wake_mutex.synchronize do
|
|
261
|
+
dirty = @dirty
|
|
262
|
+
@dirty = false
|
|
263
|
+
dirty
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def terminal_size_changed?
|
|
268
|
+
current_size = [terminal_width, terminal_height]
|
|
269
|
+
changed = @last_terminal_size != current_size
|
|
270
|
+
@last_terminal_size = current_size
|
|
271
|
+
changed
|
|
272
|
+
end
|
|
273
|
+
|
|
191
274
|
def terminal_width
|
|
192
275
|
TTY::Screen.width
|
|
193
276
|
end
|
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/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
|
@@ -21,6 +21,8 @@ module RubyRich
|
|
|
21
21
|
@selection_end = nil
|
|
22
22
|
@selected_text = ""
|
|
23
23
|
@focused = true
|
|
24
|
+
@rendered_lines_cache_key = nil
|
|
25
|
+
@rendered_lines_cache = nil
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
def focus
|
|
@@ -36,6 +38,7 @@ module RubyRich
|
|
|
36
38
|
def content=(new_content)
|
|
37
39
|
was_at_bottom = at_bottom?
|
|
38
40
|
@content = new_content
|
|
41
|
+
invalidate_rendered_lines
|
|
39
42
|
scroll_to_bottom if @auto_scroll && was_at_bottom
|
|
40
43
|
clamp_scroll
|
|
41
44
|
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
|