ruby_rich 0.4.8 → 0.4.9

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 568e2001bde24749d1410dadda7e77090963eedc48e9ad9e6d77f036e245a51f
4
- data.tar.gz: 99a40412ae33eb985e7b0e1d4dc0516b8539ca32eb79ae7ec881b673d350ecc5
3
+ metadata.gz: 852764b68219de1ce528f772a55c3e454b3556c35aa9f0c0e89c29f31c2293ba
4
+ data.tar.gz: e2827430cb5dada9ae7d8ca1cb59d9fbca4df95790404c1bcb893e7323864b95
5
5
  SHA512:
6
- metadata.gz: 187311b6561fcfea7c51af5ab44bd8b8dcbead8fabd5108d9356b07eacf82f0994ef5c6c64e00db1764a7cd8a546feb6df17c2ab119f21e11898a924a485499e
7
- data.tar.gz: ff1f361748e077f0ad6f59c30b22a8502e83a2196e7ab23a2365ee67362283036f0fb1e3da4eea102cdacfcea1c8c1ab1c08b15e8841c0da1b270182e0497e4b
6
+ metadata.gz: 95388b58b409347a55ebd1ef3dded1d3520810c7f31292bb83e14dd73192f356ec453b5688dd9dc122ae1e626ff365d6738c2d9a4de799adf53df2b2cced8916
7
+ data.tar.gz: 342ed768c7a9f04fadc1da1ec1d775fbbbdf482fc446f55ed69b4eb880159118cd36f6860c11964ddcc7371e70bcc35adcca0203183fcc558a604bd43786091e
@@ -221,27 +221,28 @@ module RubyRich
221
221
 
222
222
  return "" if header_rows.empty? || body_rows.empty?
223
223
 
224
- headers = header_rows.last
224
+ headers, fitted_body_rows = fit_table_rows(header_rows.last, body_rows)
225
225
  begin
226
226
  tbl = RubyRich::Table.new(
227
227
  headers: headers,
228
- border_style: @table_border_style
228
+ border_style: @table_border_style || :simple
229
229
  )
230
- body_rows.each do |row|
230
+ fitted_body_rows.each do |row|
231
231
  padded = row + Array.new([0, headers.length - row.length].max, "")
232
232
  tbl.add_row(padded[0...headers.length])
233
233
  end
234
234
  "#{tbl.render}\n\n"
235
235
  rescue
236
- # fallback: 纯文本表格
236
+ # fallback: plain text table
237
237
  result = "\n"
238
238
  result += header_rows.last.join(" | ")
239
239
  result += "\n#{"-" * [result.strip.length, 20].min}\n"
240
240
  body_rows.each { |row| result += row.join(" | ") + "\n" }
241
- "#{result}\n"
241
+ return "#{result}\n"
242
242
  end
243
243
  end
244
244
 
245
+ # Extract cell text from a table row element.
245
246
  def collect_row_cells(tr)
246
247
  tr.children.select { |c| [:th, :td].include?(c.type) }
247
248
  .map { |c| inline_content(c) }
@@ -255,6 +256,116 @@ module RubyRich
255
256
  inline_content(el)
256
257
  end
257
258
 
259
+ # ---- 表格宽度自适应 ----
260
+
261
+ # Fit table cell content to terminal width by normalising column counts,
262
+ # calculating natural widths, constraining to available space, and wrapping
263
+ # cell text.
264
+ def fit_table_rows(header_row, body_rows)
265
+ column_count = [header_row.length, *body_rows.map(&:length)].max.to_i
266
+ normalized_header = header_row + Array.new([0, column_count - header_row.length].max, "")
267
+ normalized_body = body_rows.map { |row| row + Array.new([0, column_count - row.length].max, "") }
268
+ natural_widths = table_natural_widths(normalized_header, normalized_body)
269
+ column_widths = constrain_table_widths(natural_widths)
270
+
271
+ headers = normalized_header.each_with_index.map { |cell, index| wrap_table_cell(table_cell_text(cell), column_widths[index]) }
272
+ rows = normalized_body.map do |row|
273
+ row.each_with_index.map { |cell, index| wrap_table_cell(table_cell_text(cell), column_widths[index]) }
274
+ end
275
+
276
+ [headers, rows]
277
+ end
278
+
279
+ # Maximum display width per column.
280
+ def table_natural_widths(header_row, body_rows)
281
+ rows = [header_row] + body_rows
282
+ return [] if rows.empty?
283
+
284
+ rows.transpose.map do |cells|
285
+ cells.map { |cell| cell_display_width(table_cell_text(cell)) }.max.to_i
286
+ end
287
+ end
288
+
289
+ # Strip ANSI escape sequences from a cell value.
290
+ def table_cell_text(cell)
291
+ cell.to_s.gsub(/\e\[[0-9;:]*m/, "")
292
+ end
293
+
294
+ # Shrink column widths proportionally to fit the terminal width.
295
+ def constrain_table_widths(natural_widths)
296
+ return natural_widths if natural_widths.empty?
297
+
298
+ border_overhead = (natural_widths.length * 3) + 1
299
+ max_table_width = [[(@width || 80).to_i - 1, 20].max, border_overhead + natural_widths.length].max
300
+ available_content_width = [max_table_width - border_overhead, natural_widths.length].max
301
+ widths = natural_widths.map { |width| [width, 1].max }
302
+ return widths if widths.sum <= available_content_width
303
+
304
+ min_width = available_content_width < natural_widths.length * 3 ? 1 : 3
305
+ while widths.sum > available_content_width
306
+ index = widths.each_with_index.select { |width, _| width > min_width }.max_by(&:first)&.last
307
+ break unless index
308
+
309
+ widths[index] -= 1
310
+ end
311
+ widths
312
+ end
313
+
314
+ # Wrap cell text to fit a given display width, splitting across newlines
315
+ # and wrapping long lines.
316
+ def wrap_table_cell(text, width)
317
+ width = [width.to_i, 1].max
318
+ text.to_s.split("\n", -1).flat_map do |line|
319
+ wrap_table_line(line, width)
320
+ end.join("\n")
321
+ end
322
+
323
+ # Wrap a single line of text to the given display width, preserving any
324
+ # ANSI escape sequences (re-emitted on each wrapped segment).
325
+ def wrap_table_line(line, width)
326
+ return [""] if line.empty?
327
+
328
+ lines = []
329
+ current = +""
330
+ current_width = 0
331
+ in_escape = false
332
+ escape = +""
333
+
334
+ line.each_char do |char|
335
+ if in_escape
336
+ escape << char
337
+ if char == "m"
338
+ current << escape
339
+ escape = +""
340
+ in_escape = false
341
+ end
342
+ next
343
+ elsif char.ord == 27
344
+ escape << char
345
+ in_escape = true
346
+ next
347
+ end
348
+
349
+ char_width = Unicode::DisplayWidth.of(char)
350
+ if current_width.positive? && current_width + char_width > width
351
+ lines << current
352
+ current = +""
353
+ current_width = 0
354
+ end
355
+ current << char
356
+ current_width += char_width
357
+ end
358
+
359
+ lines << current unless current.empty?
360
+ lines.empty? ? [""] : lines
361
+ end
362
+
363
+ # Display width of text after stripping ANSI escape sequences, taking the
364
+ # maximum across lines (for multi-line cells).
365
+ def cell_display_width(text)
366
+ text.to_s.gsub(/\e\[[0-9;:]*m/, "").split("\n").map(&:display_width).max.to_i
367
+ end
368
+
258
369
  # ---- HTML 元素处理 ----
259
370
 
260
371
  def convert_html_element(el)
@@ -409,4 +520,4 @@ module RubyRich
409
520
  self.class.render(markdown_text, @options)
410
521
  end
411
522
  end
412
- end
523
+ end
@@ -412,6 +412,8 @@ module RubyRich
412
412
  private
413
413
 
414
414
  def render_entry(entry, index)
415
+ return entry.content.to_s.split("\n", -1) if entry.metadata[:plain]
416
+
415
417
  case entry.type
416
418
  when :user
417
419
  render_plain_message(entry.content, first_prefix: "#{AnsiCode.color(:blue, true)}●#{AnsiCode.reset} ", rest_prefix: " ")
@@ -1,3 +1,3 @@
1
1
  module RubyRich
2
- VERSION = "0.4.8"
2
+ VERSION = "0.4.9"
3
3
  end
@@ -1,11 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "base64"
4
+
3
5
  module RubyRich
4
6
  class Viewport
5
7
  attr_accessor :width, :height, :scroll_top
6
8
  attr_reader :content, :selected_text
7
9
 
8
- def initialize(content = "", scrollbar: true, auto_scroll: false, scrollbar_style: :blue)
10
+ # drag_mode controls what left-click drag does in the content area:
11
+ # :viewport – drag scrolls the viewport (default, backward-compatible)
12
+ # :selection – drag selects text
13
+ DRAG_MODES = [:viewport, :selection].freeze
14
+
15
+ def initialize(content = "",
16
+ scrollbar: true, auto_scroll: false,
17
+ scrollbar_style: :blue, auto_copy: true,
18
+ drag_mode: :viewport)
9
19
  @content = content
10
20
  @scrollbar = scrollbar
11
21
  @auto_scroll = auto_scroll
@@ -22,6 +32,8 @@ module RubyRich
22
32
  @selection_end = nil
23
33
  @selected_text = ""
24
34
  @focused = true
35
+ @auto_copy = auto_copy
36
+ @drag_mode = DRAG_MODES.include?(drag_mode) ? drag_mode : :viewport
25
37
  @rendered_lines_cache_key = nil
26
38
  @rendered_lines_cache = nil
27
39
  end
@@ -88,10 +100,14 @@ module RubyRich
88
100
  true
89
101
  when :mouse_down
90
102
  return copy_selection if event_data[:button] == :right
91
-
92
- start_scrollbar_drag(event_data, layout) || start_viewport_drag(event_data, layout)
103
+ if @drag_mode == :selection
104
+ start_scrollbar_drag(event_data, layout) || start_selection(event_data, layout)
105
+ else
106
+ start_scrollbar_drag(event_data, layout) || start_viewport_drag(event_data, layout)
107
+ end
93
108
  when :mouse_drag
94
- drag_scrollbar(event_data, layout) || drag_viewport(event_data, layout) || drag_selection(event_data, layout)
109
+ drag_scrollbar(event_data, layout) ||
110
+ (@drag_mode == :selection ? drag_selection(event_data, layout) : (drag_viewport(event_data, layout) || drag_selection(event_data, layout)))
95
111
  when :mouse_up
96
112
  stop_scrollbar_drag || stop_viewport_drag || stop_selection
97
113
  else
@@ -415,7 +431,7 @@ module RubyRich
415
431
 
416
432
  @selecting = false
417
433
  @selected_text = extract_selected_text
418
- copy_selection
434
+ copy_selection if @auto_copy
419
435
  true
420
436
  end
421
437
 
@@ -477,6 +493,7 @@ module RubyRich
477
493
  escape << char
478
494
  if char == "m"
479
495
  result << escape
496
+ result << AnsiCode.inverse if active
480
497
  escape = +""
481
498
  in_escape = false
482
499
  end
@@ -531,18 +548,61 @@ module RubyRich
531
548
  text.gsub(/\e\[[0-9;:]*m/, "")
532
549
  end
533
550
 
551
+ # Copy text to the system clipboard, trying every available method in
552
+ # sequence and falling back to the OSC 52 terminal clipboard so that
553
+ # remote (SSH) sessions also work.
534
554
  def copy_to_clipboard(text)
535
- if RubyRich::Terminal.windows?
536
- copy_to_windows_clipboard(text)
537
- elsif ENV["WAYLAND_DISPLAY"]
538
- IO.popen("wl-copy", "w") { |io| io.write(text) }
539
- elsif ENV["DISPLAY"]
540
- IO.popen("xclip -selection clipboard", "w") { |io| io.write(text) }
541
- elsif RUBY_PLATFORM.match?(/darwin/)
542
- IO.popen("pbcopy", "w") { |io| io.write(text) }
555
+ text = text.to_s
556
+ return false if text.empty?
557
+ return true if RubyRich::Terminal.windows? && try_windows_clipboard(text)
558
+
559
+ clipboard_commands.each do |command|
560
+ return true if write_clipboard_command(command, text)
561
+ end
562
+
563
+ copy_to_terminal_clipboard(text)
564
+ end
565
+
566
+ # Ordered list of [command, *args] arrays to try for clipboard access.
567
+ def clipboard_commands
568
+ commands = []
569
+ commands << ["wl-copy"] if ENV["WAYLAND_DISPLAY"]
570
+ if ENV["DISPLAY"]
571
+ commands << ["xclip", "-selection", "clipboard"]
572
+ commands << ["xsel", "--clipboard", "--input"]
543
573
  end
574
+ commands << ["pbcopy"] if RUBY_PLATFORM.match?(/darwin/)
575
+ commands
576
+ end
577
+
578
+ # Attempt to write text to a given clipboard command via a pipe.
579
+ # Returns true when the subprocess exits successfully.
580
+ def write_clipboard_command(command, text)
581
+ IO.popen(command, "w") { |io| io.write(text) }
582
+ $?&.success? == true
544
583
  rescue IOError, SystemCallError
545
- nil
584
+ false
585
+ end
586
+
587
+ # Try the OS-specific Windows clipboard helper and return true when it
588
+ # succeeds (any IO/Syscall error is treated as failure).
589
+ def try_windows_clipboard(text)
590
+ copy_to_windows_clipboard(text)
591
+ true
592
+ rescue IOError, SystemCallError
593
+ false
594
+ end
595
+
596
+ # OSC 52 terminal clipboard – the only fallback that works over SSH.
597
+ # Encoded payload is emitted on stdout for the hosting terminal to
598
+ # capture.
599
+ def copy_to_terminal_clipboard(text)
600
+ encoded = Base64.strict_encode64(text.encode(Encoding::UTF_8))
601
+ $stdout.print("\e]52;c;#{encoded}\a")
602
+ $stdout.flush
603
+ true
604
+ rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError, IOError, SystemCallError
605
+ false
546
606
  end
547
607
 
548
608
  def copy_to_windows_clipboard(text)
@@ -559,4 +619,4 @@ module RubyRich
559
619
  end
560
620
  end
561
621
  end
562
- end
622
+ 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.8
4
+ version: 0.4.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - zhuang biaowei