ruby_rich 0.3.1 → 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,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyRich
4
+ class Theme
5
+ DEFAULT_ROLES = {
6
+ accent: { color: :blue, bright: true, bold: true },
7
+ body: { color: :white, bright: true },
8
+ muted: { color: :black, bright: true },
9
+ dim: { color: :black, bright: true },
10
+ thinking: { color: :white, bright: true, italic: true },
11
+ success: { color: :green, bright: true },
12
+ warning: { color: :yellow, bright: true },
13
+ error: { color: :red, bright: true },
14
+ status: { color: :cyan, bright: true }
15
+ }.freeze
16
+
17
+ attr_reader :roles, :border, :focused_border
18
+
19
+ def self.auto
20
+ ENV["COLORFGBG"].to_s.split(";").last.to_i >= 8 ? light : agent_dark
21
+ rescue
22
+ agent_dark
23
+ end
24
+
25
+ def self.agent_dark
26
+ new(
27
+ border: :blue,
28
+ focused_border: :cyan,
29
+ roles: {
30
+ accent: { color: :blue, bright: true, bold: true },
31
+ body: { color: :white, bright: true },
32
+ muted: { color: :black, bright: true },
33
+ dim: { color: :black, bright: true },
34
+ thinking: { color: :white, bright: true, italic: true },
35
+ success: { color: :green, bright: true },
36
+ warning: { color: :yellow, bright: true },
37
+ error: { color: :red, bright: true },
38
+ status: { color: :cyan, bright: true }
39
+ }
40
+ )
41
+ end
42
+
43
+ def self.light
44
+ new(
45
+ border: :blue,
46
+ focused_border: :cyan,
47
+ roles: {
48
+ accent: { color: :blue, bright: false, bold: true },
49
+ body: { color: :black, bright: false },
50
+ muted: { color: :black, bright: true },
51
+ dim: { color: :black, bright: true },
52
+ success: { color: :green, bright: false },
53
+ warning: { color: :yellow, bright: false },
54
+ error: { color: :red, bright: false },
55
+ status: { color: :cyan, bright: false }
56
+ }
57
+ )
58
+ end
59
+
60
+ def self.no_color
61
+ new(roles: DEFAULT_ROLES.transform_values { { color: :white } }, border: :white, focused_border: :white)
62
+ end
63
+
64
+ def initialize(roles: {}, border: :blue, focused_border: :cyan)
65
+ @roles = DEFAULT_ROLES.merge(roles)
66
+ @border = border
67
+ @focused_border = focused_border
68
+ end
69
+
70
+ def style(text, role = :body)
71
+ options = @roles.fetch(role, @roles[:body])
72
+ "#{style_code(options)}#{text}#{AnsiCode.reset}"
73
+ end
74
+
75
+ def color(name, bright: false)
76
+ AnsiCode.color(name, bright)
77
+ end
78
+
79
+ def panel_border(focused: false)
80
+ focused ? @focused_border : @border
81
+ end
82
+
83
+ private
84
+
85
+ def style_code(options)
86
+ code = AnsiCode.font(
87
+ options.fetch(:color, :white),
88
+ font_bright: options.fetch(:bright, false),
89
+ bold: options[:bold],
90
+ italic: options[:italic]
91
+ )
92
+ code += AnsiCode.faint if options[:faint]
93
+ code
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyRich
4
+ class ToolBlock
5
+ STATES = [:pending, :running, :done, :error, :cancelled, :denied].freeze
6
+
7
+ def initialize(entry, width: 80)
8
+ @entry = entry
9
+ @width = [width, 20].max
10
+ end
11
+
12
+ def render
13
+ name = @entry.name || @entry.metadata[:name] || "tool"
14
+ status = normalize_status(@entry.status || :pending)
15
+ header = "#{AnsiCode.color(color(status), true)}• #{status_marker(status)} #{name} #{status}#{AnsiCode.reset}"
16
+ return [header, " #{summary(@entry.content)}", " details collapsed; press Ctrl+O for full output"] if @entry.collapsed
17
+
18
+ [header] + details(@entry.content)
19
+ end
20
+
21
+ private
22
+
23
+ def normalize_status(status)
24
+ status = status.to_sym
25
+ return :error if status == :failed || status == :issue
26
+ return status if STATES.include?(status)
27
+
28
+ :pending
29
+ end
30
+
31
+ def color(status)
32
+ case status
33
+ when :done then :green
34
+ when :error, :denied then :red
35
+ when :cancelled then :yellow
36
+ else :blue
37
+ end
38
+ end
39
+
40
+ def status_marker(status)
41
+ case status
42
+ when :done then "✓"
43
+ when :error then "!"
44
+ when :cancelled then "-"
45
+ when :denied then "x"
46
+ else "▸"
47
+ end
48
+ end
49
+
50
+ def summary(content)
51
+ plain = content.to_s.gsub(/\e\[[0-9;:]*m/, "").split("\n").first.to_s
52
+ plain.empty? ? "<no output>" : plain[0, [@width - 4, 20].max]
53
+ end
54
+
55
+ def details(content)
56
+ text = content.to_s
57
+ return [" <no output>"] if text.empty?
58
+
59
+ text.split("\n").flat_map { |line| wrap(line).map { |part| " #{part}" } }
60
+ end
61
+
62
+ def wrap(line)
63
+ max_width = [@width - 2, 20].max
64
+ result = []
65
+ current = +""
66
+ width = 0
67
+ in_escape = false
68
+ line.each_char do |char|
69
+ if in_escape
70
+ current << char
71
+ in_escape = false if char == "m"
72
+ next
73
+ elsif char.ord == 27
74
+ current << char
75
+ in_escape = true
76
+ next
77
+ end
78
+
79
+ char_width = Unicode::DisplayWidth.of(char)
80
+ if width + char_width > max_width
81
+ result << current
82
+ current = +""
83
+ width = 0
84
+ end
85
+ current << char
86
+ width += char_width
87
+ end
88
+ result << current unless current.empty?
89
+ result
90
+ end
91
+ end
92
+ end