ruby_rich 0.1.0 → 0.3.0
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/ansi_code.rb +195 -0
- data/lib/ruby_rich/console.rb +103 -5
- data/lib/ruby_rich/dialog.rb +66 -0
- data/lib/ruby_rich/layout.rb +294 -0
- data/lib/ruby_rich/live.rb +118 -0
- data/lib/ruby_rich/panel.rb +183 -0
- data/lib/ruby_rich/{rich_print.rb → print.rb} +1 -1
- data/lib/ruby_rich/progress_bar.rb +3 -0
- data/lib/ruby_rich/table.rb +1 -1
- data/lib/ruby_rich/text.rb +56 -0
- data/lib/ruby_rich/version.rb +2 -2
- data/lib/ruby_rich.rb +7 -3
- metadata +10 -6
- data/lib/ruby_rich/rich_panel.rb +0 -94
- data/lib/ruby_rich/rich_text.rb +0 -80
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '009a5bcd619dc3fa975c6ab3b01d1494e002ccc760b6e8ac335026f63860671e'
|
4
|
+
data.tar.gz: 205f8d0f1cdab77854a564c43761c30113f2989216ba2ed91b7ac66e77f2387f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bedd0da9ee879bcca97fc9faf3c597cf857a05b6b8cca59004e9fbaefdbbee13b885b89902d4909a2b5718d7877201780f44418be7e2f3044f65c9840e40fe58
|
7
|
+
data.tar.gz: ac828f4bb8e7552b6a1b7310a0ba7be97d256710ce7f4aca3c523e0493b41e458fb722b33afafe01b90ab568338f14f6d56a3d546b29d191131986b07690acab
|
@@ -0,0 +1,195 @@
|
|
1
|
+
module RubyRich
|
2
|
+
class AnsiCode
|
3
|
+
ANSI_CODES = {
|
4
|
+
reset: "\e[0m",
|
5
|
+
bold: "1",
|
6
|
+
faint: "2",
|
7
|
+
italic: "3",
|
8
|
+
underline: "4",
|
9
|
+
curly_underline: "4:3",
|
10
|
+
dotted_underline: "4:4",
|
11
|
+
dashed_underline: "4:5",
|
12
|
+
double_underline: "21",
|
13
|
+
blink: "5",
|
14
|
+
rapid_blink: "6",
|
15
|
+
inverse: "7",
|
16
|
+
invisible: "8",
|
17
|
+
strikethrough: "9",
|
18
|
+
fraktur: "20",
|
19
|
+
no_blink: "25",
|
20
|
+
no_inverse: "27",
|
21
|
+
overline: "53",
|
22
|
+
color: {
|
23
|
+
black: "30",
|
24
|
+
red: "31",
|
25
|
+
green: "32",
|
26
|
+
yellow: "33",
|
27
|
+
blue: "34",
|
28
|
+
magenta: "35",
|
29
|
+
cyan: "36",
|
30
|
+
white: "37"
|
31
|
+
},
|
32
|
+
bright_color:{
|
33
|
+
black: "90",
|
34
|
+
red: "91",
|
35
|
+
green: "92",
|
36
|
+
yellow: "93",
|
37
|
+
blue: "94",
|
38
|
+
magenta: "95",
|
39
|
+
cyan: "96",
|
40
|
+
white: "97"
|
41
|
+
},
|
42
|
+
background: {
|
43
|
+
black: "40",
|
44
|
+
red: "41",
|
45
|
+
green: "42",
|
46
|
+
yellow: "43",
|
47
|
+
blue: "44",
|
48
|
+
magenta: "45",
|
49
|
+
cyan: "46",
|
50
|
+
white: "47"
|
51
|
+
},
|
52
|
+
bright_background: {
|
53
|
+
black: "100",
|
54
|
+
red: "101",
|
55
|
+
green: "102",
|
56
|
+
yellow: "103",
|
57
|
+
blue: "104",
|
58
|
+
magenta: "105",
|
59
|
+
cyan: "106",
|
60
|
+
white: "107"
|
61
|
+
}
|
62
|
+
}
|
63
|
+
|
64
|
+
def self.reset
|
65
|
+
ANSI_CODES[:reset]
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.color(color, bright=false)
|
69
|
+
if bright
|
70
|
+
"\e[#{ANSI_CODES[:bright_color][color]}m"
|
71
|
+
else
|
72
|
+
"\e[#{ANSI_CODES[:color][color]}m"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.background(color, bright=false)
|
77
|
+
if bright
|
78
|
+
"\e[#{ANSI_CODES[:bright_background][color]}m"
|
79
|
+
else
|
80
|
+
"\e[#{ANSI_CODES[:background][color]}m"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.bold
|
85
|
+
"\e[#{ANSI_CODES[:bold]}m"
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.italic
|
89
|
+
"\e[#{ANSI_CODES[:italic]}m"
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.underline(style=nil)
|
93
|
+
case style
|
94
|
+
when nil
|
95
|
+
return "\e[#{ANSI_CODES[:underline]}m"
|
96
|
+
when :double
|
97
|
+
return "\e[#{ANSI_CODES[:double_underline]}m"
|
98
|
+
when :curly
|
99
|
+
return "\e[#{ANSI_CODES[:curly_underline]}m"
|
100
|
+
when :dotted
|
101
|
+
return "\e[#{ANSI_CODES[:dotted_underline]}m"
|
102
|
+
when :dashed
|
103
|
+
return "\e[#{ANSI_CODES[:dashed_underline]}m"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.blink
|
108
|
+
"\e[#{ANSI_CODES[:blink]}m"
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.rapid_blink
|
112
|
+
"\e[#{ANSI_CODES[:rapid_blink]}m"
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.inverse
|
116
|
+
"\e[#{ANSI_CODES[:inverse]}m"
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.fraktur
|
120
|
+
"\e[#{ANSI_CODES[:fraktur]}m"
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.invisible
|
124
|
+
"\e[#{ANSI_CODES[:invisible]}m"
|
125
|
+
end
|
126
|
+
|
127
|
+
def self.strikethrough
|
128
|
+
"\e[#{ANSI_CODES[:strikethrough]}m"
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.overline
|
132
|
+
"\e[#{ANSI_CODES[:overline]}m"
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.no_blink
|
136
|
+
"\e[#{ANSI_CODES[:no_blink]}m"
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.no_inverse
|
140
|
+
"\e[#{ANSI_CODES[:no_inverse]}m"
|
141
|
+
end
|
142
|
+
|
143
|
+
def self.font(font_color,
|
144
|
+
font_bright: false,
|
145
|
+
background: nil,
|
146
|
+
background_bright: false,
|
147
|
+
bold: false,
|
148
|
+
italic: false,
|
149
|
+
underline: false,
|
150
|
+
underline_style: nil,
|
151
|
+
strikethrough: false,
|
152
|
+
overline: false
|
153
|
+
)
|
154
|
+
code = if font_bright
|
155
|
+
"\e[#{ANSI_CODES[:bright_color][font_color]}"
|
156
|
+
else
|
157
|
+
"\e[#{ANSI_CODES[:color][font_color]}"
|
158
|
+
end
|
159
|
+
if background
|
160
|
+
code += ";" + if background_bright
|
161
|
+
"#{ANSI_CODES[:bright_background][background]}"
|
162
|
+
else
|
163
|
+
"#{ANSI_CODES[:background][background]}"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
if bold
|
167
|
+
code += ";" + ANSI_CODES[:bold]
|
168
|
+
end
|
169
|
+
if italic
|
170
|
+
code += ";" + ANSI_CODES[:italic]
|
171
|
+
end
|
172
|
+
if underline
|
173
|
+
case underline_style
|
174
|
+
when nil
|
175
|
+
code += ";" + ANSI_CODES[:underline]
|
176
|
+
when :double
|
177
|
+
code += ";" + ANSI_CODES[:double_underline]
|
178
|
+
when :curly
|
179
|
+
code += ";" + ANSI_CODES[:curly_underline]
|
180
|
+
when :dotted
|
181
|
+
code += ";" + ANSI_CODES[:dotted_underline]
|
182
|
+
when :dashed
|
183
|
+
code += ";" + ANSI_CODES[:dashed_underline]
|
184
|
+
end
|
185
|
+
end
|
186
|
+
if strikethrough
|
187
|
+
code += ";" + ANSI_CODES[:strikethrough]
|
188
|
+
end
|
189
|
+
if overline
|
190
|
+
code += ";" + ANSI_CODES[:overline]
|
191
|
+
end
|
192
|
+
return code+"m"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
data/lib/ruby_rich/console.rb
CHANGED
@@ -1,9 +1,31 @@
|
|
1
|
+
require 'io/console'
|
2
|
+
|
1
3
|
module RubyRich
|
2
4
|
class Console
|
5
|
+
ESCAPE_SEQUENCES = {
|
6
|
+
# 方向键
|
7
|
+
'[A' => :up, '[B' => :down,
|
8
|
+
'[C' => :right, '[D' => :left,
|
9
|
+
# 功能键
|
10
|
+
'OP' => :f1, 'OQ' => :f2, 'OR' => :f3, 'OS' => :f4,
|
11
|
+
'[15~' => :f5, '[17~' => :f6, '[18~' => :f7,
|
12
|
+
'[19~' => :f8, '[20~' => :f9, '[21~' => :f10,
|
13
|
+
'[23~' => :f11, '[24~' => :f12,
|
14
|
+
# 添加媒体键示例
|
15
|
+
'[1~' => :home, '[4~' => :end,
|
16
|
+
# 添加 macOS 功能键
|
17
|
+
'[25~' => :audio_mute,
|
18
|
+
# 其他
|
19
|
+
'[5~' => :page_up, '[6~' => :page_down,
|
20
|
+
'[H' => :home, '[F' => :end,
|
21
|
+
'[2~' => :insert, '[3~' => :delete
|
22
|
+
}.freeze
|
23
|
+
|
3
24
|
def initialize
|
4
25
|
@lines = []
|
5
26
|
@buffer = []
|
6
27
|
@layout = { spacing: 1, align: :left }
|
28
|
+
@styles = {}
|
7
29
|
end
|
8
30
|
|
9
31
|
def set_layout(spacing: 1, align: :left)
|
@@ -11,12 +33,89 @@ module RubyRich
|
|
11
33
|
@layout[:align] = align
|
12
34
|
end
|
13
35
|
|
36
|
+
def style(name, **attributes)
|
37
|
+
@styles[name] = attributes
|
38
|
+
end
|
39
|
+
|
40
|
+
def print(*objects, sep: ' ', end_char: "\n")
|
41
|
+
line_text = objects.map(&:to_s).join(sep)
|
42
|
+
add_line(line_text)
|
43
|
+
render
|
44
|
+
end
|
45
|
+
|
46
|
+
def log(message, *objects, sep: ' ', end_char: "\n")
|
47
|
+
timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S")
|
48
|
+
log_message = "[#{timestamp}] LOG: #{message} #{objects.map(&:to_s).join(sep)}"
|
49
|
+
add_line(log_message)
|
50
|
+
render
|
51
|
+
end
|
52
|
+
|
53
|
+
def rule(title: nil, characters: '#', style: 'bold')
|
54
|
+
rule_line = characters * 80
|
55
|
+
if title
|
56
|
+
formatted_title = " #{title} ".center(80, characters)
|
57
|
+
add_line(formatted_title)
|
58
|
+
else
|
59
|
+
add_line(rule_line)
|
60
|
+
end
|
61
|
+
render
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.raw
|
65
|
+
old_state = `stty -g`
|
66
|
+
system('stty raw -echo -icanon isig') rescue nil
|
67
|
+
yield
|
68
|
+
ensure
|
69
|
+
system("stty #{old_state}") rescue nil
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.clear
|
73
|
+
system('clear')
|
74
|
+
end
|
75
|
+
|
76
|
+
def get_key(input: $stdin)
|
77
|
+
input.raw(intr: true) do |io|
|
78
|
+
char = io.getch
|
79
|
+
# 优先处理回车键(ASCII 13 = \r,ASCII 10 = \n)
|
80
|
+
if char == "\r" || char == "\n"
|
81
|
+
return {:name=>:enter}
|
82
|
+
end
|
83
|
+
# 单独处理 Tab 键(ASCII 9)
|
84
|
+
if char == "\t"
|
85
|
+
return {:name=>:tab}
|
86
|
+
elsif char.ord == 0x07F
|
87
|
+
return {:name=>:backspace}
|
88
|
+
elsif char == "\e" # 检测到转义序列
|
89
|
+
sequence = ''
|
90
|
+
begin
|
91
|
+
while (c = io.read_nonblock(1))
|
92
|
+
sequence << c
|
93
|
+
end
|
94
|
+
rescue IO::WaitReadable
|
95
|
+
retry if IO.select([io], nil, nil, 0.01)
|
96
|
+
rescue EOFError
|
97
|
+
end
|
98
|
+
if sequence.empty?
|
99
|
+
return {:name => :escape}
|
100
|
+
else
|
101
|
+
return {:name => ESCAPE_SEQUENCES[sequence]} || {:name => :escape}
|
102
|
+
end
|
103
|
+
# 处理 Ctrl 组合键(排除 Tab 和回车)
|
104
|
+
elsif char.ord.between?(1, 8) || char.ord.between?(10, 26)
|
105
|
+
ctrl_char = (char.ord + 64).chr.downcase
|
106
|
+
return {:name =>"ctrl_#{ctrl_char}".to_sym}
|
107
|
+
else
|
108
|
+
{:name => :string, :value => char}
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
14
113
|
def add_line(text)
|
15
114
|
@lines << text
|
16
115
|
end
|
17
116
|
|
18
117
|
def clear
|
19
|
-
print "\e[H\e[2J"
|
118
|
+
Kernel.print "\e[H\e[2J"
|
20
119
|
end
|
21
120
|
|
22
121
|
def render
|
@@ -24,14 +123,13 @@ module RubyRich
|
|
24
123
|
@lines.each_with_index do |line, index|
|
25
124
|
formatted_line = format_line(line)
|
26
125
|
@buffer << formatted_line
|
27
|
-
puts formatted_line
|
28
|
-
puts "\n" * @layout[:spacing] if index < @lines.size - 1
|
126
|
+
Kernel.puts formatted_line
|
127
|
+
Kernel.puts "\n" * @layout[:spacing] if index < @lines.size - 1
|
29
128
|
end
|
30
129
|
end
|
31
130
|
|
32
131
|
def update_line(index, text)
|
33
132
|
return unless index.between?(0, @lines.size - 1)
|
34
|
-
|
35
133
|
@lines[index] = text
|
36
134
|
render
|
37
135
|
end
|
@@ -50,4 +148,4 @@ module RubyRich
|
|
50
148
|
end
|
51
149
|
end
|
52
150
|
end
|
53
|
-
end
|
151
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module RubyRich
|
2
|
+
class Dialog
|
3
|
+
attr_accessor :title, :content, :buttons, :live
|
4
|
+
attr_accessor :width, :height
|
5
|
+
|
6
|
+
def initialize(title: "", content: "", width: 48, height: 8, buttons: [:ok])
|
7
|
+
@width = width
|
8
|
+
@height = height
|
9
|
+
terminal_width = `tput cols`.to_i
|
10
|
+
terminal_height = `tput lines`.to_i
|
11
|
+
@event_listeners = {}
|
12
|
+
@layout = RubyRich::Layout.new(name: :title, width: width, height: height)
|
13
|
+
@panel = RubyRich::Panel.new("", title: title, border_style: :white)
|
14
|
+
@layout.update_content(@panel)
|
15
|
+
@layout.calculate_dimensions(terminal_width, terminal_height)
|
16
|
+
@button_str = build_button(buttons)
|
17
|
+
@panel.content = " \n \n#{content}"+AnsiCode.reset+"\n \n \n" + " "*((@panel.inner_width - @button_str.display_width)/2) + @button_str
|
18
|
+
end
|
19
|
+
|
20
|
+
def content=(content)
|
21
|
+
@panel.content = " \n \n#{content}"+AnsiCode.reset+"\n \n \n" + " "*((@panel.inner_width - @button_str.display_width)/2) + @button_str
|
22
|
+
end
|
23
|
+
|
24
|
+
def build_button(buttons)
|
25
|
+
str = ""
|
26
|
+
buttons.each do |btn|
|
27
|
+
case btn
|
28
|
+
when :ok
|
29
|
+
str += " "+AnsiCode.color(:blue) + "OK(enter)"+ AnsiCode.reset
|
30
|
+
when :cancel
|
31
|
+
str += " "+AnsiCode.color(:red) + "Cancel(esc)"+ AnsiCode.reset
|
32
|
+
end
|
33
|
+
end
|
34
|
+
str.strip
|
35
|
+
end
|
36
|
+
|
37
|
+
def render_to_buffer
|
38
|
+
@layout.render_to_buffer
|
39
|
+
end
|
40
|
+
|
41
|
+
def key(event_name, priority = 0, &block)
|
42
|
+
unless @event_listeners[event_name]
|
43
|
+
@event_listeners[event_name] = []
|
44
|
+
end
|
45
|
+
@event_listeners[event_name] << { priority: priority, block: block }
|
46
|
+
@event_listeners[event_name].sort_by! { |l| -l[:priority] } # Higher priority first
|
47
|
+
end
|
48
|
+
|
49
|
+
def on(event_name, &block)
|
50
|
+
if event_name==:close
|
51
|
+
@event_listeners[event_name] = [{block: block}]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def notify_listeners(event_data)
|
56
|
+
event_name = event_data[:name]
|
57
|
+
result = nil
|
58
|
+
if @event_listeners[event_name]
|
59
|
+
@event_listeners[event_name].each do |listener|
|
60
|
+
result = listener[:block].call(event_data, @live)
|
61
|
+
end
|
62
|
+
return result
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,294 @@
|
|
1
|
+
module RubyRich
|
2
|
+
class Layout
|
3
|
+
attr_accessor :name, :ratio, :size, :children, :content, :parent, :live, :dialog
|
4
|
+
attr_accessor :x_offset, :y_offset, :width, :height, :show
|
5
|
+
attr_reader :split_direction
|
6
|
+
|
7
|
+
def initialize(name: nil, ratio: 1, size: nil, width: nil, height: nil)
|
8
|
+
@name = name
|
9
|
+
@ratio = ratio
|
10
|
+
@size = size
|
11
|
+
@children = []
|
12
|
+
@content = nil
|
13
|
+
@parent = nil
|
14
|
+
@x_offset = 0
|
15
|
+
@y_offset = 0
|
16
|
+
@width = width if width
|
17
|
+
@height = height if height
|
18
|
+
@split_direction = nil
|
19
|
+
@show = true
|
20
|
+
@event_listeners = {}
|
21
|
+
@event_intercepted = false
|
22
|
+
end
|
23
|
+
|
24
|
+
def key(event_name, priority = 0, &block)
|
25
|
+
unless @event_listeners[event_name]
|
26
|
+
@event_listeners[event_name] = []
|
27
|
+
end
|
28
|
+
@event_listeners[event_name] << { priority: priority, block: block }
|
29
|
+
@event_listeners[event_name].sort_by! { |l| -l[:priority] } # Higher priority first
|
30
|
+
end
|
31
|
+
|
32
|
+
def show=(flag)
|
33
|
+
@show = flag
|
34
|
+
@event_intercepted = !flag
|
35
|
+
end
|
36
|
+
|
37
|
+
def notify_listeners(event_data)
|
38
|
+
return if @event_intercepted
|
39
|
+
if @dialog
|
40
|
+
@dialog.notify_listeners(event_data)
|
41
|
+
else
|
42
|
+
event_name = event_data[:name]
|
43
|
+
if @event_listeners[event_name]
|
44
|
+
@event_listeners[event_name].each do |listener|
|
45
|
+
next if @event_intercepted
|
46
|
+
result = listener[:block].call(event_data, self.root.live)
|
47
|
+
if result == true
|
48
|
+
@event_intercepted = true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
unless @event_intercepted
|
54
|
+
@children.each do |child|
|
55
|
+
child.notify_listeners(event_data)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def root
|
62
|
+
@parent ? @parent.root : self
|
63
|
+
end
|
64
|
+
|
65
|
+
def split_row(*layouts)
|
66
|
+
@split_direction = :row
|
67
|
+
layouts.each { |l| l.parent = self }
|
68
|
+
@children.concat(layouts)
|
69
|
+
end
|
70
|
+
|
71
|
+
def split_column(*layouts)
|
72
|
+
@split_direction = :column
|
73
|
+
layouts.each { |l| l.parent = self }
|
74
|
+
@children.concat(layouts)
|
75
|
+
end
|
76
|
+
|
77
|
+
def add_child(layout)
|
78
|
+
@children << layout
|
79
|
+
end
|
80
|
+
|
81
|
+
def update_content(content)
|
82
|
+
@content = content
|
83
|
+
end
|
84
|
+
|
85
|
+
def calculate_dimensions(terminal_width, terminal_height)
|
86
|
+
@x_offset = 0
|
87
|
+
@y_offset = 0
|
88
|
+
calculate_node_dimensions(terminal_width, terminal_height)
|
89
|
+
end
|
90
|
+
|
91
|
+
def [](name)
|
92
|
+
find_by_name(name)
|
93
|
+
end
|
94
|
+
|
95
|
+
def find_by_name(name)
|
96
|
+
return self if @name == name
|
97
|
+
@children.each do |child|
|
98
|
+
result = child.find_by_name(name)
|
99
|
+
return result if result
|
100
|
+
end
|
101
|
+
nil
|
102
|
+
end
|
103
|
+
|
104
|
+
def show_dialog(dialog)
|
105
|
+
@dialog = dialog
|
106
|
+
end
|
107
|
+
|
108
|
+
def hide_dialog
|
109
|
+
@dialog.notify_listeners({:name=>:close})
|
110
|
+
@dialog = nil
|
111
|
+
end
|
112
|
+
|
113
|
+
def render
|
114
|
+
# 将缓冲区转换为字符串(每行用换行符连接)
|
115
|
+
buffer = render_to_buffer
|
116
|
+
buffer.map { |line| line.compact.join("") }.join("\n")
|
117
|
+
end
|
118
|
+
|
119
|
+
def render_to_buffer
|
120
|
+
# 初始化缓冲区(二维数组,每个元素代表一个字符)
|
121
|
+
buffer = Array.new(@height) { Array.new(@width, " ") }
|
122
|
+
# 递归填充内容到缓冲区
|
123
|
+
render_into(buffer)
|
124
|
+
render_dialog_into(buffer) if @dialog
|
125
|
+
return buffer
|
126
|
+
end
|
127
|
+
|
128
|
+
def draw
|
129
|
+
puts render
|
130
|
+
end
|
131
|
+
|
132
|
+
def render_dialog_into(buffer)
|
133
|
+
start_x = (@width - 2 - @dialog.width) / 2 + 1
|
134
|
+
start_y = (@height - 2 - @dialog.height) / 2 + 1
|
135
|
+
dialog_buffer = @dialog.render_to_buffer
|
136
|
+
buffer.each_with_index do |arr, y|
|
137
|
+
arr.each_with_index do |char, x|
|
138
|
+
if x >= start_x && y >= start_y
|
139
|
+
if y-start_y <= dialog_buffer.size-1 && x-start_x <= dialog_buffer[y-start_y].size-1
|
140
|
+
buffer[y][x] = dialog_buffer[y-start_y][x-start_x]
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def render_into(buffer)
|
148
|
+
children.each { |child| child.render_into(buffer) } if children
|
149
|
+
return unless content
|
150
|
+
content_lines = if content.is_a?(String)
|
151
|
+
content.split("\n")[0...height]
|
152
|
+
else
|
153
|
+
content.render[0...height]
|
154
|
+
end
|
155
|
+
|
156
|
+
content_lines.each_with_index do |line, line_index|
|
157
|
+
y_pos = y_offset + line_index
|
158
|
+
next if y_pos >= buffer.size
|
159
|
+
|
160
|
+
in_escape = false
|
161
|
+
escape_char = ""
|
162
|
+
char_width = 0 # 初始宽度调整为0,方便位置计算
|
163
|
+
line.each_char do |char|
|
164
|
+
# 处理ANSI转义码
|
165
|
+
if in_escape
|
166
|
+
escape_char += char
|
167
|
+
in_escape = false if char == 'm'
|
168
|
+
if escape_char=="\e[0m"
|
169
|
+
escape_char = ""
|
170
|
+
end
|
171
|
+
next
|
172
|
+
elsif char.ord == 27 # 检测到转义开始符\e
|
173
|
+
in_escape = true
|
174
|
+
escape_char += char
|
175
|
+
next
|
176
|
+
end
|
177
|
+
|
178
|
+
# 计算字符宽度
|
179
|
+
char_w = case char.ord
|
180
|
+
when 0x0000..0x007F then 1 # 英文字符
|
181
|
+
when 0x4E00..0x9FFF then 2 # 中文字符
|
182
|
+
else Unicode::DisplayWidth.of(char)
|
183
|
+
end
|
184
|
+
# 计算字符的起始位置
|
185
|
+
x_start = x_offset + char_width
|
186
|
+
|
187
|
+
# 超出右边界则跳过
|
188
|
+
next if x_start >= buffer[y_pos].size
|
189
|
+
|
190
|
+
# 处理字符渲染(中文字符可能占用多个位置)
|
191
|
+
char_w.times do |i|
|
192
|
+
x_pos = x_start + i
|
193
|
+
break if x_pos >= buffer[y_pos].size # 超出右边界停止
|
194
|
+
unless escape_char.empty?
|
195
|
+
char = escape_char + char + "\e[0m" # 每次都记录字符的实际颜色
|
196
|
+
end
|
197
|
+
buffer[y_pos][x_pos] = char unless i > 0 # 中文字符仅在第一个位置写入,避免覆盖
|
198
|
+
buffer[y_pos][x_pos+1] = nil if char_w == 2
|
199
|
+
end
|
200
|
+
char_width += char_w # 更新累计宽度
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def calculate_node_dimensions(available_width, available_height)
|
206
|
+
# 只在未设置宽度时计算宽度
|
207
|
+
@width ||= if @size
|
208
|
+
[@size, available_width].min
|
209
|
+
else
|
210
|
+
available_width
|
211
|
+
end
|
212
|
+
|
213
|
+
# 只在未设置高度时计算高度
|
214
|
+
@height ||= if @size
|
215
|
+
[@size, available_height].min
|
216
|
+
else
|
217
|
+
available_height
|
218
|
+
end
|
219
|
+
|
220
|
+
if @content.class == RubyRich::Panel
|
221
|
+
@content.width = @width
|
222
|
+
@content.height = @height
|
223
|
+
end
|
224
|
+
|
225
|
+
return if @children.empty?
|
226
|
+
|
227
|
+
case @split_direction
|
228
|
+
when :row
|
229
|
+
remaining_width = @width
|
230
|
+
fixed_children, flexible_children = @children.partition { |c| c.size }
|
231
|
+
|
232
|
+
fixed_children.each do |child|
|
233
|
+
child_width = [child.size, remaining_width].min
|
234
|
+
child.width = child_width
|
235
|
+
remaining_width -= child_width
|
236
|
+
end
|
237
|
+
|
238
|
+
total_ratio = flexible_children.sum(&:ratio)
|
239
|
+
if total_ratio > 0
|
240
|
+
ratio_width = remaining_width.to_f / total_ratio
|
241
|
+
flexible_children.each do |child|
|
242
|
+
child_width = (child.ratio * ratio_width).floor
|
243
|
+
child.width = child_width
|
244
|
+
remaining_width -= child_width
|
245
|
+
end
|
246
|
+
|
247
|
+
flexible_children.last.width += remaining_width if remaining_width > 0
|
248
|
+
end
|
249
|
+
|
250
|
+
@children.each { |child| child.height = @height }
|
251
|
+
|
252
|
+
current_x = @x_offset
|
253
|
+
@children.each do |child|
|
254
|
+
child.x_offset = current_x
|
255
|
+
child.y_offset = @y_offset
|
256
|
+
current_x += child.width
|
257
|
+
child.calculate_node_dimensions(child.width, child.height)
|
258
|
+
end
|
259
|
+
|
260
|
+
when :column
|
261
|
+
remaining_height = @height
|
262
|
+
fixed_children, flexible_children = @children.partition { |c| c.size }
|
263
|
+
|
264
|
+
fixed_children.each do |child|
|
265
|
+
child_height = [child.size, remaining_height].min
|
266
|
+
child.height = child_height
|
267
|
+
remaining_height -= child_height
|
268
|
+
end
|
269
|
+
|
270
|
+
total_ratio = flexible_children.sum(&:ratio)
|
271
|
+
if total_ratio > 0
|
272
|
+
ratio_height = remaining_height.to_f / total_ratio
|
273
|
+
flexible_children.each do |child|
|
274
|
+
child_height = (child.ratio * ratio_height).floor
|
275
|
+
child.height = child_height
|
276
|
+
remaining_height -= child_height
|
277
|
+
end
|
278
|
+
|
279
|
+
flexible_children.last.height += remaining_height if remaining_height > 0
|
280
|
+
end
|
281
|
+
|
282
|
+
@children.each { |child| child.width = @width }
|
283
|
+
|
284
|
+
current_y = @y_offset
|
285
|
+
@children.each do |child|
|
286
|
+
child.x_offset = @x_offset
|
287
|
+
child.y_offset = current_y
|
288
|
+
current_y += child.height
|
289
|
+
child.calculate_node_dimensions(child.width, child.height)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'io/console'
|
2
|
+
require "tty-screen"
|
3
|
+
require "tty-cursor"
|
4
|
+
|
5
|
+
module RubyRich
|
6
|
+
|
7
|
+
class CacheRender
|
8
|
+
def initialize
|
9
|
+
@cache = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def print_with_pos(x,y,char)
|
13
|
+
print "\e[?25l" # 隐藏光标
|
14
|
+
print "\e[#{y};#{x}H" # 移动光标到左上角
|
15
|
+
print char
|
16
|
+
end
|
17
|
+
|
18
|
+
def draw(buffer)
|
19
|
+
unless @cache
|
20
|
+
system("clear")
|
21
|
+
print_with_pos(0,0,buffer.map { |line| line.compact.join("") }.join("\n"))
|
22
|
+
@cache = buffer
|
23
|
+
else
|
24
|
+
buffer.each_with_index do |arr, y|
|
25
|
+
arr.each_with_index do |char, x|
|
26
|
+
if @cache[y][x] != char
|
27
|
+
print_with_pos(x + 1 , y + 1 , char)
|
28
|
+
@cache[y][x] = char
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Live
|
37
|
+
attr_accessor :params, :app, :listening, :layout
|
38
|
+
class << self
|
39
|
+
def start(layout, refresh_rate: 30, &proc)
|
40
|
+
setup_terminal
|
41
|
+
live = new(layout, refresh_rate)
|
42
|
+
proc.call(live) if proc
|
43
|
+
live.run(proc)
|
44
|
+
rescue => e
|
45
|
+
puts e.message
|
46
|
+
ensure
|
47
|
+
restore_terminal
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def setup_terminal
|
53
|
+
@original_state = `stty -g`
|
54
|
+
system("stty -echo")
|
55
|
+
end
|
56
|
+
|
57
|
+
def restore_terminal
|
58
|
+
system("stty #{@original_state}")
|
59
|
+
print TTY::Cursor.show
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def initialize(layout, refresh_rate)
|
64
|
+
@layout = layout
|
65
|
+
@layout.live = self
|
66
|
+
@refresh_rate = refresh_rate
|
67
|
+
@running = true
|
68
|
+
@last_frame = Time.now
|
69
|
+
@cursor = TTY::Cursor
|
70
|
+
@render = CacheRender.new
|
71
|
+
@console = RubyRich::Console.new
|
72
|
+
@params = {}
|
73
|
+
end
|
74
|
+
|
75
|
+
def run(proc = nil)
|
76
|
+
while @running
|
77
|
+
render_frame
|
78
|
+
if @listening
|
79
|
+
event_data = @console.get_key()
|
80
|
+
@layout.notify_listeners(event_data)
|
81
|
+
end
|
82
|
+
sleep 1.0 / @refresh_rate
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def stop
|
87
|
+
@running = false
|
88
|
+
system("clear")
|
89
|
+
end
|
90
|
+
|
91
|
+
def move_cursor(x,y)
|
92
|
+
print @cursor.move_to(x, y)
|
93
|
+
end
|
94
|
+
|
95
|
+
def find_layout(name)
|
96
|
+
@layout[name]
|
97
|
+
end
|
98
|
+
|
99
|
+
def find_panel(name)
|
100
|
+
@layout[name].content
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def render_frame
|
106
|
+
@layout.calculate_dimensions(terminal_width, terminal_height)
|
107
|
+
@render.draw(@layout.render_to_buffer)
|
108
|
+
end
|
109
|
+
|
110
|
+
def terminal_width
|
111
|
+
TTY::Screen.width
|
112
|
+
end
|
113
|
+
|
114
|
+
def terminal_height
|
115
|
+
TTY::Screen.height
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
module RubyRich
|
2
|
+
class Panel
|
3
|
+
attr_accessor :width, :height, :content, :line_pos, :border_style, :title
|
4
|
+
attr_accessor :title_align, :content_changed
|
5
|
+
|
6
|
+
def initialize(content = "", title: nil, border_style: :white, title_align: :center)
|
7
|
+
@content = content
|
8
|
+
@title = title
|
9
|
+
@border_style = border_style
|
10
|
+
@width = 0
|
11
|
+
@height = 0
|
12
|
+
@line_pos = 0
|
13
|
+
@title_align = title_align
|
14
|
+
end
|
15
|
+
|
16
|
+
def inner_width
|
17
|
+
@width - 2 # Account for border characters
|
18
|
+
end
|
19
|
+
|
20
|
+
def page_up
|
21
|
+
@line_pos -= ( @height - 4 )
|
22
|
+
if @line_pos < 0
|
23
|
+
@line_pos = 0
|
24
|
+
end
|
25
|
+
@content_changed = false
|
26
|
+
end
|
27
|
+
|
28
|
+
def page_down
|
29
|
+
unless @content.empty?
|
30
|
+
content_lines = wrap_content(@content)
|
31
|
+
@line_pos += ( @height - 4 )
|
32
|
+
if @line_pos + ( @height - 4 ) > content_lines.size
|
33
|
+
@line_pos = content_lines.size - @height + 2
|
34
|
+
end
|
35
|
+
@content_changed = false
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def home
|
40
|
+
@line_pos = 0
|
41
|
+
@content_changed = false
|
42
|
+
end
|
43
|
+
|
44
|
+
def end
|
45
|
+
unless @content.empty?
|
46
|
+
content_lines = wrap_content(@content)
|
47
|
+
@line_pos = content_lines.size - @height + 2
|
48
|
+
@content_changed = false
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def render
|
53
|
+
lines = []
|
54
|
+
color_code = AnsiCode.color(@border_style) || AnsiCode.color(:white)
|
55
|
+
reset_code = AnsiCode.reset
|
56
|
+
|
57
|
+
# Top border
|
58
|
+
top_border = color_code + "┌"
|
59
|
+
if @title
|
60
|
+
title_text = "[ #{@title} ]"
|
61
|
+
bar_width = @width - @title.display_width-6
|
62
|
+
case @title_align
|
63
|
+
when :left
|
64
|
+
top_border += title_text + '─' * bar_width
|
65
|
+
when :center
|
66
|
+
top_border += '─' * (bar_width/2) + title_text + '─' * (bar_width - bar_width/2)
|
67
|
+
when :right
|
68
|
+
top_border += '─' * bar_width + title_text
|
69
|
+
end
|
70
|
+
else
|
71
|
+
top_border += '─' * (@width - 2)
|
72
|
+
end
|
73
|
+
top_border += "┐" + reset_code
|
74
|
+
lines << top_border
|
75
|
+
|
76
|
+
# Content area
|
77
|
+
content_lines = wrap_content(@content)
|
78
|
+
if @line_pos==0
|
79
|
+
if @content_changed == false
|
80
|
+
if content_lines.size > @height - 2
|
81
|
+
content_lines=content_lines[0..@height - 3]
|
82
|
+
end
|
83
|
+
else
|
84
|
+
if content_lines.size > @height - 2
|
85
|
+
@line_pos = content_lines.size - @height + 2
|
86
|
+
content_lines=content_lines[@line_pos..-1]
|
87
|
+
@content_changed = false
|
88
|
+
end
|
89
|
+
end
|
90
|
+
else
|
91
|
+
if @line_pos+@height-2 >= content_lines.size
|
92
|
+
content_lines=content_lines[@line_pos..-1]
|
93
|
+
else
|
94
|
+
content_lines=content_lines[@line_pos..@line_pos+@height-3]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
content_lines.each do |line|
|
99
|
+
lines << color_code + "│" + reset_code +
|
100
|
+
line + " "*(@width - line.display_width - 2) +
|
101
|
+
color_code + "│" + reset_code
|
102
|
+
end
|
103
|
+
|
104
|
+
# Fill remaining vertical space
|
105
|
+
remaining_lines = @height - 2 - content_lines.size
|
106
|
+
remaining_lines.times do
|
107
|
+
lines << color_code + "│" + reset_code +
|
108
|
+
" " * (@width - 2) +
|
109
|
+
color_code + "│" + reset_code
|
110
|
+
end
|
111
|
+
|
112
|
+
# Bottom border
|
113
|
+
lines << color_code + "└" + "─" * (@width - 2) + "┘" + reset_code
|
114
|
+
|
115
|
+
lines
|
116
|
+
end
|
117
|
+
|
118
|
+
def content=(new_content)
|
119
|
+
@content = new_content
|
120
|
+
content_lines = wrap_content(@content)
|
121
|
+
if content_lines.size > @height - 2
|
122
|
+
@line_pos = content_lines.size - @height + 2
|
123
|
+
end
|
124
|
+
@content_changed = true
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
def split_text_by_width(text)
|
130
|
+
result = []
|
131
|
+
current_line = ""
|
132
|
+
current_width = 0
|
133
|
+
|
134
|
+
# Split text into tokens of ANSI codes and regular text
|
135
|
+
tokens = text.scan(/(\e\[[0-9;]*m)|(.)/)
|
136
|
+
.map { |m| m.compact.first }
|
137
|
+
|
138
|
+
tokens.each do |token|
|
139
|
+
# Calculate width for regular text, ANSI codes have 0 width
|
140
|
+
if token.start_with?("\e[")
|
141
|
+
token_width = 0
|
142
|
+
else
|
143
|
+
token_width = token.chars.sum { |c| Unicode::DisplayWidth.of(c) }
|
144
|
+
end
|
145
|
+
|
146
|
+
if current_width + token_width <= @width - 4
|
147
|
+
current_line += token
|
148
|
+
current_width += token_width
|
149
|
+
else
|
150
|
+
result << current_line
|
151
|
+
current_line = token
|
152
|
+
current_width = token_width
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Add remaining line
|
157
|
+
result << current_line unless current_line.empty?
|
158
|
+
|
159
|
+
result
|
160
|
+
end
|
161
|
+
|
162
|
+
def wrap_content(text)
|
163
|
+
text.split("\n").flat_map do |line|
|
164
|
+
split_text_by_width(line)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Extend String to remove ANSI codes for alignment
|
171
|
+
class String
|
172
|
+
def uncolorize
|
173
|
+
gsub(/\e\[[0-9;]*m/, '')
|
174
|
+
end
|
175
|
+
|
176
|
+
def display_width
|
177
|
+
width = 0
|
178
|
+
self.uncolorize.each_char do |char|
|
179
|
+
width += Unicode::DisplayWidth.of(char)
|
180
|
+
end
|
181
|
+
width
|
182
|
+
end
|
183
|
+
end
|
data/lib/ruby_rich/table.rb
CHANGED
@@ -0,0 +1,56 @@
|
|
1
|
+
module RubyRich
|
2
|
+
class RichText
|
3
|
+
# 默认主题
|
4
|
+
@@theme = {
|
5
|
+
error: { color: :red, bold: true },
|
6
|
+
success: { color: :green, bold: true },
|
7
|
+
info: { color: :cyan },
|
8
|
+
warning: { color: :yellow, bold: true }
|
9
|
+
}
|
10
|
+
|
11
|
+
def self.set_theme(new_theme)
|
12
|
+
@@theme.merge!(new_theme)
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(text, style: nil)
|
16
|
+
@text = text
|
17
|
+
@styles = []
|
18
|
+
apply_theme(style) if style
|
19
|
+
end
|
20
|
+
|
21
|
+
def style(color: :white,
|
22
|
+
font_bright: false,
|
23
|
+
background: nil,
|
24
|
+
background_bright: false,
|
25
|
+
bold: false,
|
26
|
+
italic: false,
|
27
|
+
underline: false,
|
28
|
+
underline_style: nil,
|
29
|
+
strikethrough: false,
|
30
|
+
overline: false
|
31
|
+
)
|
32
|
+
@styles << AnsiCode.font(color, font_bright, background, background_bright, bold, italic, underline, underline_style, strikethrough, overline)
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def render
|
37
|
+
"#{@styles.join}#{@text}#{AnsiCode.reset}"
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def add_style(code, error_message)
|
43
|
+
if code
|
44
|
+
@styles << code
|
45
|
+
else
|
46
|
+
raise ArgumentError, error_message
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def apply_theme(style)
|
51
|
+
theme_styles = @@theme[style]
|
52
|
+
raise ArgumentError, "Undefined theme style: #{style}" unless theme_styles
|
53
|
+
style(**theme_styles)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/ruby_rich/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
1
|
module RubyRich
|
2
|
-
VERSION = "0.
|
3
|
-
end
|
2
|
+
VERSION = "0.3.0"
|
3
|
+
end
|
data/lib/ruby_rich.rb
CHANGED
@@ -10,9 +10,13 @@ require 'redcarpet'
|
|
10
10
|
require_relative 'ruby_rich/console'
|
11
11
|
require_relative 'ruby_rich/table'
|
12
12
|
require_relative 'ruby_rich/progress_bar'
|
13
|
-
require_relative 'ruby_rich/
|
14
|
-
require_relative 'ruby_rich/
|
15
|
-
require_relative 'ruby_rich/
|
13
|
+
require_relative 'ruby_rich/layout'
|
14
|
+
require_relative 'ruby_rich/live'
|
15
|
+
require_relative 'ruby_rich/text'
|
16
|
+
require_relative 'ruby_rich/print'
|
17
|
+
require_relative 'ruby_rich/panel'
|
18
|
+
require_relative 'ruby_rich/dialog'
|
19
|
+
require_relative 'ruby_rich/ansi_code'
|
16
20
|
require_relative 'ruby_rich/version'
|
17
21
|
|
18
22
|
# 定义主模块
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby_rich
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- zhuang biaowei
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-03-10 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: rake
|
@@ -45,12 +45,16 @@ extensions: []
|
|
45
45
|
extra_rdoc_files: []
|
46
46
|
files:
|
47
47
|
- lib/ruby_rich.rb
|
48
|
+
- lib/ruby_rich/ansi_code.rb
|
48
49
|
- lib/ruby_rich/console.rb
|
50
|
+
- lib/ruby_rich/dialog.rb
|
51
|
+
- lib/ruby_rich/layout.rb
|
52
|
+
- lib/ruby_rich/live.rb
|
53
|
+
- lib/ruby_rich/panel.rb
|
54
|
+
- lib/ruby_rich/print.rb
|
49
55
|
- lib/ruby_rich/progress_bar.rb
|
50
|
-
- lib/ruby_rich/rich_panel.rb
|
51
|
-
- lib/ruby_rich/rich_print.rb
|
52
|
-
- lib/ruby_rich/rich_text.rb
|
53
56
|
- lib/ruby_rich/table.rb
|
57
|
+
- lib/ruby_rich/text.rb
|
54
58
|
- lib/ruby_rich/version.rb
|
55
59
|
homepage: https://github.com/zhuangbiaowei/ruby_rich
|
56
60
|
licenses:
|
@@ -70,7 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
70
74
|
- !ruby/object:Gem::Version
|
71
75
|
version: '0'
|
72
76
|
requirements: []
|
73
|
-
rubygems_version: 3.6.
|
77
|
+
rubygems_version: 3.6.4
|
74
78
|
specification_version: 4
|
75
79
|
summary: Rich text formatting and console output for Ruby
|
76
80
|
test_files: []
|
data/lib/ruby_rich/rich_panel.rb
DELETED
@@ -1,94 +0,0 @@
|
|
1
|
-
module RubyRich
|
2
|
-
class RichPanel
|
3
|
-
# ANSI escape codes for styling
|
4
|
-
ANSI_CODES = {
|
5
|
-
reset: "\e[0m",
|
6
|
-
bold: "\e[1m",
|
7
|
-
underline: "\e[4m",
|
8
|
-
color: {
|
9
|
-
black: "\e[30m",
|
10
|
-
red: "\e[31m",
|
11
|
-
green: "\e[32m",
|
12
|
-
yellow: "\e[33m",
|
13
|
-
blue: "\e[34m",
|
14
|
-
magenta: "\e[35m",
|
15
|
-
cyan: "\e[36m",
|
16
|
-
white: "\e[37m"
|
17
|
-
},
|
18
|
-
background: {
|
19
|
-
black: "\e[40m",
|
20
|
-
red: "\e[41m",
|
21
|
-
green: "\e[42m",
|
22
|
-
yellow: "\e[43m",
|
23
|
-
blue: "\e[44m",
|
24
|
-
magenta: "\e[45m",
|
25
|
-
cyan: "\e[46m",
|
26
|
-
white: "\e[47m"
|
27
|
-
}
|
28
|
-
}
|
29
|
-
|
30
|
-
attr_accessor :title, :content, :border_color, :title_color, :footer
|
31
|
-
|
32
|
-
def initialize(content, title: nil, footer: nil, border_color: :white, title_color: :white)
|
33
|
-
@content = content.is_a?(String) ? content.split("\n") : content
|
34
|
-
@title = title
|
35
|
-
@footer = footer
|
36
|
-
@border_color = border_color
|
37
|
-
@title_color = title_color
|
38
|
-
end
|
39
|
-
|
40
|
-
def render
|
41
|
-
content_lines = format_content
|
42
|
-
panel_width = calculate_panel_width(content_lines)
|
43
|
-
|
44
|
-
lines = []
|
45
|
-
lines << top_border(panel_width)
|
46
|
-
lines += content_lines.map { |line| format_line(line, panel_width) }
|
47
|
-
lines << bottom_border(panel_width)
|
48
|
-
|
49
|
-
lines.join("\n")
|
50
|
-
end
|
51
|
-
|
52
|
-
private
|
53
|
-
|
54
|
-
def top_border(width)
|
55
|
-
title_text = @title ? colorize(" #{@title} ", @title_color) : ""
|
56
|
-
padding = (width - title_text.uncolorize.length - 2) / 2
|
57
|
-
"#{colorize("╭", @border_color)}#{colorize("─" * padding, @border_color)}#{title_text}#{colorize("─" * (width - title_text.uncolorize.length - padding - 2), @border_color)}#{colorize("╮", @border_color)}"
|
58
|
-
end
|
59
|
-
|
60
|
-
def bottom_border(width)
|
61
|
-
footer_text = @footer ? colorize(" #{@footer} ", @title_color) : ""
|
62
|
-
padding = (width - footer_text.uncolorize.length - 2) / 2
|
63
|
-
"#{colorize("╰", @border_color)}#{colorize("─" * padding, @border_color)}#{footer_text}#{colorize("─" * (width - footer_text.uncolorize.length - padding - 2), @border_color)}#{colorize("╯", @border_color)}"
|
64
|
-
end
|
65
|
-
|
66
|
-
def format_line(line, width)
|
67
|
-
"#{colorize("│", @border_color)} #{line.ljust(width - 4)} #{colorize("│", @border_color)}"
|
68
|
-
end
|
69
|
-
|
70
|
-
def format_content
|
71
|
-
@content.map(&:strip)
|
72
|
-
end
|
73
|
-
|
74
|
-
def calculate_panel_width(content_lines)
|
75
|
-
[
|
76
|
-
@title ? @title.uncolorize.length + 4 : 0,
|
77
|
-
@footer ? @footer.uncolorize.length + 4 : 0,
|
78
|
-
content_lines.map(&:length).max + 4
|
79
|
-
].max
|
80
|
-
end
|
81
|
-
|
82
|
-
def colorize(text, color)
|
83
|
-
code = ANSI_CODES[:color][color] || ""
|
84
|
-
"#{code}#{text}#{ANSI_CODES[:reset]}"
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
# Extend String to remove ANSI codes for alignment
|
90
|
-
class String
|
91
|
-
def uncolorize
|
92
|
-
gsub(/\e\[[0-9;]*m/, '')
|
93
|
-
end
|
94
|
-
end
|
data/lib/ruby_rich/rich_text.rb
DELETED
@@ -1,80 +0,0 @@
|
|
1
|
-
module RubyRich
|
2
|
-
class RichText
|
3
|
-
# 默认主题
|
4
|
-
@@theme = {
|
5
|
-
error: { color: :red, bold: true },
|
6
|
-
success: { color: :green, bold: true },
|
7
|
-
info: { color: :cyan },
|
8
|
-
warning: { color: :yellow, bold: true }
|
9
|
-
}
|
10
|
-
|
11
|
-
# ANSI 转义码常量
|
12
|
-
ANSI_CODES = {
|
13
|
-
reset: "\e[0m",
|
14
|
-
bold: "\e[1m",
|
15
|
-
italic: "\e[3m",
|
16
|
-
underline: "\e[4m",
|
17
|
-
blink: "\e[5m",
|
18
|
-
color: {
|
19
|
-
black: "\e[30m",
|
20
|
-
red: "\e[31m",
|
21
|
-
green: "\e[32m",
|
22
|
-
yellow: "\e[33m",
|
23
|
-
blue: "\e[34m",
|
24
|
-
magenta: "\e[35m",
|
25
|
-
cyan: "\e[36m",
|
26
|
-
white: "\e[37m"
|
27
|
-
},
|
28
|
-
background: {
|
29
|
-
black: "\e[40m",
|
30
|
-
red: "\e[41m",
|
31
|
-
green: "\e[42m",
|
32
|
-
yellow: "\e[43m",
|
33
|
-
blue: "\e[44m",
|
34
|
-
magenta: "\e[45m",
|
35
|
-
cyan: "\e[46m",
|
36
|
-
white: "\e[47m"
|
37
|
-
}
|
38
|
-
}
|
39
|
-
|
40
|
-
def self.set_theme(new_theme)
|
41
|
-
@@theme.merge!(new_theme)
|
42
|
-
end
|
43
|
-
|
44
|
-
def initialize(text, style: nil)
|
45
|
-
@text = text
|
46
|
-
@styles = []
|
47
|
-
apply_theme(style) if style
|
48
|
-
end
|
49
|
-
|
50
|
-
def style(color: nil, background: nil, bold: false, italic: false, underline: false, blink: false)
|
51
|
-
@styles << ANSI_CODES[:color][color] if color
|
52
|
-
@styles << ANSI_CODES[:background][background] if background
|
53
|
-
@styles << ANSI_CODES[:bold] if bold
|
54
|
-
@styles << ANSI_CODES[:italic] if italic
|
55
|
-
@styles << ANSI_CODES[:underline] if underline
|
56
|
-
@styles << ANSI_CODES[:blink] if blink
|
57
|
-
self
|
58
|
-
end
|
59
|
-
|
60
|
-
def render
|
61
|
-
"#{@styles.join}#{@text}#{ANSI_CODES[:reset]}"
|
62
|
-
end
|
63
|
-
|
64
|
-
private
|
65
|
-
|
66
|
-
def add_style(code, error_message)
|
67
|
-
if code
|
68
|
-
@styles << code
|
69
|
-
else
|
70
|
-
raise ArgumentError, error_message
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
def apply_theme(style)
|
75
|
-
theme_styles = @@theme[style]
|
76
|
-
raise ArgumentError, "Undefined theme style: #{style}" unless theme_styles
|
77
|
-
style(**theme_styles)
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|