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
|