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.
@@ -0,0 +1,468 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyRich
4
+ class Viewport
5
+ attr_accessor :width, :height, :scroll_top
6
+ attr_reader :content, :selected_text
7
+
8
+ def initialize(content = "", scrollbar: true, auto_scroll: false, scrollbar_style: :blue)
9
+ @content = content
10
+ @scrollbar = scrollbar
11
+ @auto_scroll = auto_scroll
12
+ @scrollbar_style = scrollbar_style
13
+ @width = 0
14
+ @height = 0
15
+ @scroll_top = 0
16
+ @dragging_scrollbar = false
17
+ @drag_start_y = nil
18
+ @drag_start_scroll_top = nil
19
+ @selecting = false
20
+ @selection_start = nil
21
+ @selection_end = nil
22
+ @selected_text = ""
23
+ @focused = true
24
+ end
25
+
26
+ def focus
27
+ @focused = true
28
+ self
29
+ end
30
+
31
+ def blur
32
+ @focused = false
33
+ self
34
+ end
35
+
36
+ def content=(new_content)
37
+ was_at_bottom = at_bottom?
38
+ @content = new_content
39
+ scroll_to_bottom if @auto_scroll && was_at_bottom
40
+ clamp_scroll
41
+ end
42
+
43
+ def attach(layout, priority: 100)
44
+ [:page_up, :page_down, :home, :end, :up, :down].each do |event_name|
45
+ layout.key(event_name, priority) do |event_data, _live|
46
+ handle_event(event_data, layout)
47
+ false
48
+ end
49
+ end
50
+
51
+ [:mouse_wheel, :mouse_down, :mouse_drag, :mouse_up].each do |event_name|
52
+ layout.key(event_name, priority) do |event_data, _live|
53
+ handle_event(event_data, layout)
54
+ end
55
+ end
56
+
57
+ self
58
+ end
59
+
60
+ def handle_event(event_data, layout = nil)
61
+ return false if keyboard_event?(event_data) && !@focused
62
+ return false if mouse_event?(event_data) && !@focused
63
+
64
+ case event_data[:name]
65
+ when :page_up
66
+ scroll_by(-page_size)
67
+ false
68
+ when :page_down
69
+ scroll_by(page_size)
70
+ false
71
+ when :home
72
+ scroll_to(0)
73
+ false
74
+ when :end
75
+ scroll_to_bottom
76
+ false
77
+ when :up
78
+ scroll_by(-1)
79
+ false
80
+ when :down
81
+ scroll_by(1)
82
+ false
83
+ when :mouse_wheel
84
+ scroll_by(event_data[:direction] == :down ? 3 : -3)
85
+ true
86
+ when :mouse_down
87
+ return copy_selection if event_data[:button] == :right
88
+
89
+ start_scrollbar_drag(event_data, layout) || start_selection(event_data, layout)
90
+ when :mouse_drag
91
+ drag_scrollbar(event_data, layout) || drag_selection(event_data, layout)
92
+ when :mouse_up
93
+ stop_scrollbar_drag || stop_selection
94
+ else
95
+ false
96
+ end
97
+ end
98
+
99
+ def render
100
+ clamp_scroll
101
+ return [] if @height.to_i <= 0 || @width.to_i <= 0
102
+
103
+ visible_width = content_width
104
+ visible = rendered_lines[@scroll_top, @height] || []
105
+ lines = Array.new(@height) { "" }
106
+
107
+ @height.times do |index|
108
+ absolute_line = @scroll_top + index
109
+ lines[index] = apply_selection(fit_line(visible[index].to_s, visible_width), absolute_line)
110
+ end
111
+
112
+ return lines unless show_scrollbar?
113
+
114
+ scrollbar = render_scrollbar
115
+ lines.each_with_index.map { |line, index| line + scrollbar[index] }
116
+ end
117
+
118
+ def scroll_by(delta)
119
+ scroll_to(@scroll_top + delta)
120
+ end
121
+
122
+ def scroll_to(line)
123
+ @scroll_top = line.to_i
124
+ clamp_scroll
125
+ end
126
+
127
+ def scroll_to_bottom
128
+ @scroll_top = max_scroll_top
129
+ clamp_scroll
130
+ end
131
+
132
+ def at_bottom?
133
+ @scroll_top >= max_scroll_top
134
+ end
135
+
136
+ def max_scroll_top
137
+ [rendered_lines.length - @height, 0].max
138
+ end
139
+
140
+ private
141
+
142
+ def rendered_lines
143
+ @rendered_lines = normalize_lines(@content)
144
+ end
145
+
146
+ def keyboard_event?(event_data)
147
+ event_data[:type] == :key
148
+ end
149
+
150
+ def mouse_event?(event_data)
151
+ event_data[:type] == :mouse
152
+ end
153
+
154
+ def normalize_lines(value)
155
+ value.width = content_width if value.respond_to?(:width=)
156
+
157
+ rendered = if value.respond_to?(:render)
158
+ value.render
159
+ else
160
+ value
161
+ end
162
+
163
+ case rendered
164
+ when String
165
+ rendered.split("\n")
166
+ when Array
167
+ rendered
168
+ else
169
+ rendered.to_s.split("\n")
170
+ end
171
+ end
172
+
173
+ def page_size
174
+ [@height - 1, 1].max
175
+ end
176
+
177
+ def content_width
178
+ @scrollbar ? [@width - 1, 1].max : @width
179
+ end
180
+
181
+ def show_scrollbar?
182
+ @scrollbar && @width.positive?
183
+ end
184
+
185
+ def render_scrollbar
186
+ lines = Array.new(@height) { track_char }
187
+ return lines if @height <= 0
188
+
189
+ thumb_size.times do |offset|
190
+ index = thumb_top + offset
191
+ lines[index] = thumb_char if index.between?(0, @height - 1)
192
+ end
193
+
194
+ lines
195
+ end
196
+
197
+ def thumb_size
198
+ total = rendered_lines.length
199
+ return @height if total <= @height
200
+
201
+ [(@height.to_f * @height / total).ceil, 1].max
202
+ end
203
+
204
+ def thumb_top
205
+ total_scroll = max_scroll_top
206
+ return 0 if total_scroll.zero?
207
+
208
+ travel = [@height - thumb_size, 0].max
209
+ (@scroll_top.to_f / total_scroll * travel).round
210
+ end
211
+
212
+ def track_char
213
+ "#{AnsiCode.color(:black, true)}│#{AnsiCode.reset}"
214
+ end
215
+
216
+ def thumb_char
217
+ "#{AnsiCode.color(@scrollbar_style, true)}│#{AnsiCode.reset}"
218
+ end
219
+
220
+ def fit_line(line, target_width)
221
+ plain_width = display_width(line)
222
+ return line + (" " * (target_width - plain_width)) if plain_width <= target_width
223
+
224
+ truncate_display(line, target_width)
225
+ end
226
+
227
+ def truncate_display(line, target_width)
228
+ result = +""
229
+ width = 0
230
+ in_escape = false
231
+ escape = +""
232
+
233
+ line.each_char do |char|
234
+ if in_escape
235
+ escape << char
236
+ if char == "m"
237
+ result << escape
238
+ escape = +""
239
+ in_escape = false
240
+ end
241
+ next
242
+ elsif char.ord == 27
243
+ escape << char
244
+ in_escape = true
245
+ next
246
+ end
247
+
248
+ char_width = Unicode::DisplayWidth.of(char)
249
+ break if width + char_width > target_width
250
+
251
+ result << char
252
+ width += char_width
253
+ end
254
+
255
+ result
256
+ end
257
+
258
+ def display_width(line)
259
+ line.gsub(/\e\[[0-9;:]*m/, "").chars.sum { |char| Unicode::DisplayWidth.of(char) }
260
+ end
261
+
262
+ def clamp_scroll
263
+ @scroll_top = [[@scroll_top, 0].max, max_scroll_top].min
264
+ end
265
+
266
+ def start_scrollbar_drag(event_data, layout)
267
+ return false unless layout
268
+ return false unless event_data[:x] == layout.x_offset + @width - 1
269
+
270
+ @dragging_scrollbar = true
271
+ @drag_start_y = event_data[:y]
272
+ @drag_start_scroll_top = @scroll_top
273
+ scroll_to(scroll_top_for_y(event_data[:y], layout))
274
+ true
275
+ end
276
+
277
+ def start_selection(event_data, layout)
278
+ return false unless layout
279
+ return false if event_data[:x] >= layout.x_offset + content_width
280
+ return false unless event_data[:y].between?(layout.y_offset, layout.y_offset + @height - 1)
281
+
282
+ @selecting = true
283
+ @selection_start = mouse_position(event_data, layout)
284
+ @selection_end = @selection_start
285
+ @selected_text = ""
286
+ true
287
+ end
288
+
289
+ def drag_scrollbar(event_data, layout)
290
+ return false unless @dragging_scrollbar && layout
291
+
292
+ if @drag_start_y
293
+ travel = [@height - thumb_size, 1].max
294
+ scroll_travel = max_scroll_top
295
+ delta_y = event_data[:y] - @drag_start_y
296
+ scroll_to(@drag_start_scroll_top + (delta_y.to_f / travel * scroll_travel).round)
297
+ else
298
+ scroll_to(scroll_top_for_y(event_data[:y], layout))
299
+ end
300
+
301
+ true
302
+ end
303
+
304
+ def drag_selection(event_data, layout)
305
+ return false unless @selecting && layout
306
+
307
+ @selection_end = mouse_position(event_data, layout)
308
+ true
309
+ end
310
+
311
+ def stop_scrollbar_drag
312
+ was_dragging = @dragging_scrollbar
313
+ @dragging_scrollbar = false
314
+ @drag_start_y = nil
315
+ @drag_start_scroll_top = nil
316
+ was_dragging
317
+ end
318
+
319
+ def stop_selection
320
+ return false unless @selecting
321
+
322
+ @selecting = false
323
+ @selected_text = extract_selected_text
324
+ copy_selection
325
+ true
326
+ end
327
+
328
+ def copy_selection
329
+ return false if @selected_text.to_s.empty?
330
+
331
+ copy_to_clipboard(@selected_text)
332
+ true
333
+ end
334
+
335
+ def scroll_top_for_y(y, layout)
336
+ relative_y = [[y - layout.y_offset, 0].max, @height - 1].min
337
+ travel = [@height - thumb_size, 1].max
338
+ (relative_y.to_f / travel * max_scroll_top).round
339
+ end
340
+
341
+ def mouse_position(event_data, layout)
342
+ {
343
+ line: @scroll_top + [[event_data[:y] - layout.y_offset, 0].max, @height - 1].min,
344
+ col: [[event_data[:x] - layout.x_offset, 0].max, content_width].min
345
+ }
346
+ end
347
+
348
+ def normalized_selection
349
+ return nil unless @selection_start && @selection_end
350
+
351
+ a = @selection_start
352
+ b = @selection_end
353
+ if a[:line] < b[:line] || (a[:line] == b[:line] && a[:col] <= b[:col])
354
+ [a, b]
355
+ else
356
+ [b, a]
357
+ end
358
+ end
359
+
360
+ def apply_selection(line, absolute_line)
361
+ range = normalized_selection
362
+ return line unless range
363
+
364
+ start_pos, end_pos = range
365
+ return line if absolute_line < start_pos[:line] || absolute_line > end_pos[:line]
366
+
367
+ start_col = absolute_line == start_pos[:line] ? start_pos[:col] : 0
368
+ end_col = absolute_line == end_pos[:line] ? end_pos[:col] : content_width
369
+ highlight_display_range(line, start_col, end_col)
370
+ end
371
+
372
+ def highlight_display_range(line, start_col, end_col)
373
+ return line if end_col <= start_col
374
+
375
+ result = +""
376
+ width = 0
377
+ active = false
378
+ in_escape = false
379
+ escape = +""
380
+
381
+ line.each_char do |char|
382
+ if in_escape
383
+ escape << char
384
+ if char == "m"
385
+ result << escape
386
+ escape = +""
387
+ in_escape = false
388
+ end
389
+ next
390
+ elsif char.ord == 27
391
+ escape << char
392
+ in_escape = true
393
+ next
394
+ end
395
+
396
+ char_width = Unicode::DisplayWidth.of(char)
397
+ should_highlight = width < end_col && width + char_width > start_col
398
+ if should_highlight && !active
399
+ result << AnsiCode.inverse
400
+ active = true
401
+ elsif !should_highlight && active
402
+ result << AnsiCode.reset
403
+ active = false
404
+ end
405
+ result << char
406
+ width += char_width
407
+ end
408
+ result << AnsiCode.reset if active
409
+ result
410
+ end
411
+
412
+ def extract_selected_text
413
+ range = normalized_selection
414
+ return "" unless range
415
+
416
+ start_pos, end_pos = range
417
+ (start_pos[:line]..end_pos[:line]).map do |line_index|
418
+ line = strip_ansi(rendered_lines[line_index].to_s)
419
+ start_col = line_index == start_pos[:line] ? start_pos[:col] : 0
420
+ end_col = line_index == end_pos[:line] ? end_pos[:col] : display_width(line)
421
+ slice_display_range(line, start_col, end_col).rstrip
422
+ end.join("\n")
423
+ end
424
+
425
+ def slice_display_range(line, start_col, end_col)
426
+ result = +""
427
+ width = 0
428
+ line.each_char do |char|
429
+ char_width = Unicode::DisplayWidth.of(char)
430
+ result << char if width < end_col && width + char_width > start_col
431
+ width += char_width
432
+ end
433
+ result
434
+ end
435
+
436
+ def strip_ansi(text)
437
+ text.gsub(/\e\[[0-9;:]*m/, "")
438
+ end
439
+
440
+ def copy_to_clipboard(text)
441
+ if RubyRich::Terminal.windows?
442
+ copy_to_windows_clipboard(text)
443
+ elsif ENV["WAYLAND_DISPLAY"]
444
+ IO.popen("wl-copy", "w") { |io| io.write(text) }
445
+ elsif ENV["DISPLAY"]
446
+ IO.popen("xclip -selection clipboard", "w") { |io| io.write(text) }
447
+ elsif RUBY_PLATFORM.match?(/darwin/)
448
+ IO.popen("pbcopy", "w") { |io| io.write(text) }
449
+ end
450
+ rescue IOError, SystemCallError
451
+ nil
452
+ end
453
+
454
+ def copy_to_windows_clipboard(text)
455
+ script = "[Console]::InputEncoding=[System.Text.UTF8Encoding]::new($false); Set-Clipboard -Value ([Console]::In.ReadToEnd())"
456
+ IO.popen(["powershell", "-NoProfile", "-NonInteractive", "-Command", script], "w") do |io|
457
+ io.set_encoding(Encoding::UTF_8)
458
+ io.write(text.to_s.encode(Encoding::UTF_8))
459
+ end
460
+ rescue IOError, SystemCallError
461
+ IO.popen("clip", "w") do |io|
462
+ io.binmode
463
+ io.write("\uFEFF".encode("UTF-16LE"))
464
+ io.write(text.to_s.encode("UTF-16LE"))
465
+ end
466
+ end
467
+ end
468
+ end
data/lib/ruby_rich.rb CHANGED
@@ -1,15 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # 加载所有依赖的 gem
3
+ # Load all dependency gems
4
+ require 'logger'
4
5
  require 'rouge'
5
6
  require 'tty-cursor'
6
7
  require 'tty-screen'
7
8
  require 'redcarpet'
8
9
 
9
- # 加载所有内部模块
10
+ # Load all internal modules
10
11
  require_relative 'ruby_rich/console'
12
+ require_relative 'ruby_rich/event'
13
+ require_relative 'ruby_rich/terminal'
11
14
  require_relative 'ruby_rich/table'
12
15
  require_relative 'ruby_rich/progress_bar'
16
+ require_relative 'ruby_rich/progress_manager'
13
17
  require_relative 'ruby_rich/layout'
14
18
  require_relative 'ruby_rich/live'
15
19
  require_relative 'ruby_rich/text'
@@ -22,49 +26,70 @@ require_relative 'ruby_rich/tree'
22
26
  require_relative 'ruby_rich/columns'
23
27
  require_relative 'ruby_rich/status'
24
28
  require_relative 'ruby_rich/ansi_code'
29
+ require_relative 'ruby_rich/theme'
30
+ require_relative 'ruby_rich/slash_input'
31
+ require_relative 'ruby_rich/attachment'
32
+ require_relative 'ruby_rich/line_editor'
33
+ require_relative 'ruby_rich/tool_block'
34
+ require_relative 'ruby_rich/viewport'
35
+ require_relative 'ruby_rich/composer'
36
+ require_relative 'ruby_rich/focus_manager'
37
+ require_relative 'ruby_rich/transcript'
38
+ require_relative 'ruby_rich/sidebar'
39
+ require_relative 'ruby_rich/app_shell'
40
+ require_relative 'ruby_rich/agent_shell'
25
41
  require_relative 'ruby_rich/version'
26
42
 
27
- # 定义主模块
43
+ # Define main module
28
44
  module RubyRich
29
45
  class Error < StandardError; end
30
-
31
- # 提供一个便捷方法来创建控制台实例
46
+
47
+ # Provide a convenient method to create console instance
32
48
  def self.console
33
49
  @console ||= Console.new
34
50
  end
35
51
 
36
- # 提供一个便捷方法来创建富文本
52
+ # Provide a convenient method to create rich text
37
53
  def self.text(content = '')
38
54
  RichText.new(content)
39
55
  end
40
56
 
41
- # 提供一个便捷方法来创建表格
57
+ # Provide a convenient method to create table
42
58
  def self.table(border_style: :none)
43
59
  Table.new(border_style: border_style)
44
60
  end
45
61
 
46
- # 提供一个便捷方法来进行语法高亮
62
+ # Provide a convenient method for syntax highlighting
47
63
  def self.syntax(code, language = nil, theme: :default)
48
64
  Syntax.highlight(code, language, theme: theme)
49
65
  end
50
66
 
51
- # 提供一个便捷方法来渲染 Markdown
67
+ # Provide a convenient method to render Markdown
52
68
  def self.markdown(text, options = {})
53
69
  Markdown.render(text, options)
54
70
  end
55
71
 
56
- # 提供一个便捷方法来创建树形结构
72
+ # Provide a convenient method to create tree structure
57
73
  def self.tree(root_name = 'Root', style: :default)
58
74
  Tree.new(root_name, style: style)
59
75
  end
60
76
 
61
- # 提供一个便捷方法来创建多列布局
77
+ # Provide a convenient method to create multi-column layout
62
78
  def self.columns(total_width: 80, gutter_width: 2)
63
79
  Columns.new(total_width: total_width, gutter_width: gutter_width)
64
80
  end
65
81
 
66
- # 提供一个便捷方法来创建状态指示器
82
+ # Provide a convenient method to create status indicator
67
83
  def self.status(type, **options)
68
84
  Status.indicator(type, **options)
69
85
  end
70
- end
86
+
87
+ def self.logger=(logger)
88
+ @logger = logger
89
+ end
90
+ def self.logger
91
+ @logger ||= Logger.new($stdout).tap do |log|
92
+ log.progname = "Ruby Rich"
93
+ end
94
+ end
95
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_rich
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - zhuang biaowei
@@ -51,20 +51,6 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: 4.5.2
54
- - !ruby/object:Gem::Dependency
55
- name: unicode-display_width
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - "~>"
59
- - !ruby/object:Gem::Version
60
- version: 3.1.4
61
- type: :runtime
62
- prerelease: false
63
- version_requirements: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - "~>"
66
- - !ruby/object:Gem::Version
67
- version: 3.1.4
68
54
  - !ruby/object:Gem::Dependency
69
55
  name: tty-cursor
70
56
  requirement: !ruby/object:Gem::Requirement
@@ -94,33 +80,33 @@ dependencies:
94
80
  - !ruby/object:Gem::Version
95
81
  version: 0.8.2
96
82
  - !ruby/object:Gem::Dependency
97
- name: tty-reader
83
+ name: redcarpet
98
84
  requirement: !ruby/object:Gem::Requirement
99
85
  requirements:
100
86
  - - "~>"
101
87
  - !ruby/object:Gem::Version
102
- version: 0.9.0
88
+ version: 3.6.1
103
89
  type: :runtime
104
90
  prerelease: false
105
91
  version_requirements: !ruby/object:Gem::Requirement
106
92
  requirements:
107
93
  - - "~>"
108
94
  - !ruby/object:Gem::Version
109
- version: 0.9.0
95
+ version: 3.6.1
110
96
  - !ruby/object:Gem::Dependency
111
- name: redcarpet
97
+ name: unicode-display_width
112
98
  requirement: !ruby/object:Gem::Requirement
113
99
  requirements:
114
100
  - - "~>"
115
101
  - !ruby/object:Gem::Version
116
- version: 3.6.1
102
+ version: 3.1.4
117
103
  type: :runtime
118
104
  prerelease: false
119
105
  version_requirements: !ruby/object:Gem::Requirement
120
106
  requirements:
121
107
  - - "~>"
122
108
  - !ruby/object:Gem::Version
123
- version: 3.6.1
109
+ version: 3.1.4
124
110
  description: A Ruby gem providing rich text formatting, progress bars, tables and
125
111
  other console output enhancements
126
112
  email: zbw@kaiyuanshe.org
@@ -129,22 +115,37 @@ extensions: []
129
115
  extra_rdoc_files: []
130
116
  files:
131
117
  - lib/ruby_rich.rb
118
+ - lib/ruby_rich/agent_shell.rb
132
119
  - lib/ruby_rich/ansi_code.rb
120
+ - lib/ruby_rich/app_shell.rb
121
+ - lib/ruby_rich/attachment.rb
133
122
  - lib/ruby_rich/columns.rb
123
+ - lib/ruby_rich/composer.rb
134
124
  - lib/ruby_rich/console.rb
135
125
  - lib/ruby_rich/dialog.rb
126
+ - lib/ruby_rich/event.rb
127
+ - lib/ruby_rich/focus_manager.rb
136
128
  - lib/ruby_rich/layout.rb
129
+ - lib/ruby_rich/line_editor.rb
137
130
  - lib/ruby_rich/live.rb
138
131
  - lib/ruby_rich/markdown.rb
139
132
  - lib/ruby_rich/panel.rb
140
133
  - lib/ruby_rich/print.rb
141
134
  - lib/ruby_rich/progress_bar.rb
135
+ - lib/ruby_rich/progress_manager.rb
136
+ - lib/ruby_rich/sidebar.rb
137
+ - lib/ruby_rich/slash_input.rb
142
138
  - lib/ruby_rich/status.rb
143
139
  - lib/ruby_rich/syntax.rb
144
140
  - lib/ruby_rich/table.rb
141
+ - lib/ruby_rich/terminal.rb
145
142
  - lib/ruby_rich/text.rb
143
+ - lib/ruby_rich/theme.rb
144
+ - lib/ruby_rich/tool_block.rb
145
+ - lib/ruby_rich/transcript.rb
146
146
  - lib/ruby_rich/tree.rb
147
147
  - lib/ruby_rich/version.rb
148
+ - lib/ruby_rich/viewport.rb
148
149
  homepage: https://github.com/zhuangbiaowei/ruby_rich
149
150
  licenses:
150
151
  - MIT
@@ -163,7 +164,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
163
164
  - !ruby/object:Gem::Version
164
165
  version: '0'
165
166
  requirements: []
166
- rubygems_version: 3.6.9
167
+ rubygems_version: 4.0.10
167
168
  specification_version: 4
168
169
  summary: Rich text formatting and console output for Ruby
169
170
  test_files: []