ruby_rich 0.3.0 → 0.4.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/columns.rb +244 -0
- data/lib/ruby_rich/console.rb +20 -5
- data/lib/ruby_rich/markdown.rb +343 -0
- data/lib/ruby_rich/panel.rb +7 -2
- data/lib/ruby_rich/progress_bar.rb +229 -11
- data/lib/ruby_rich/status.rb +246 -0
- data/lib/ruby_rich/syntax.rb +171 -0
- data/lib/ruby_rich/table.rb +155 -9
- data/lib/ruby_rich/text.rb +111 -1
- data/lib/ruby_rich/tree.rb +200 -0
- data/lib/ruby_rich/version.rb +1 -1
- data/lib/ruby_rich.rb +33 -3
- metadata +92 -3
|
@@ -1,29 +1,247 @@
|
|
|
1
1
|
module RubyRich
|
|
2
2
|
class ProgressBar
|
|
3
3
|
|
|
4
|
-
attr_reader :progress
|
|
4
|
+
attr_reader :progress, :total, :start_time
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
# 进度条样式
|
|
7
|
+
STYLES = {
|
|
8
|
+
default: { filled: '=', empty: ' ', prefix: '[', suffix: ']' },
|
|
9
|
+
blocks: { filled: '█', empty: '░', prefix: '[', suffix: ']' },
|
|
10
|
+
arrows: { filled: '>', empty: '-', prefix: '[', suffix: ']' },
|
|
11
|
+
dots: { filled: '●', empty: '○', prefix: '(', suffix: ')' },
|
|
12
|
+
line: { filled: '━', empty: '─', prefix: '│', suffix: '│' },
|
|
13
|
+
gradient: { filled: ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'], empty: ' ', prefix: '[', suffix: ']' }
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def initialize(total, width: 50, style: :default, title: nil, show_percentage: true, show_rate: false, show_eta: false)
|
|
7
17
|
@total = total
|
|
8
18
|
@progress = 0
|
|
9
19
|
@width = width
|
|
10
20
|
@style = style
|
|
21
|
+
@title = title
|
|
22
|
+
@show_percentage = show_percentage
|
|
23
|
+
@show_rate = show_rate
|
|
24
|
+
@show_eta = show_eta
|
|
25
|
+
@start_time = nil
|
|
26
|
+
@last_update_time = nil
|
|
27
|
+
@update_history = []
|
|
11
28
|
end
|
|
12
29
|
|
|
13
|
-
def
|
|
30
|
+
def start
|
|
31
|
+
@start_time = Time.now
|
|
32
|
+
@last_update_time = @start_time
|
|
33
|
+
render
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def advance(amount = 1)
|
|
37
|
+
@start_time ||= Time.now
|
|
14
38
|
@progress += amount
|
|
15
|
-
@progress = @
|
|
39
|
+
@progress = [@progress, @total].min
|
|
40
|
+
|
|
41
|
+
current_time = Time.now
|
|
42
|
+
@update_history << { time: current_time, progress: @progress }
|
|
43
|
+
|
|
44
|
+
# 保留最近的几个更新用于计算速率
|
|
45
|
+
@update_history = @update_history.last(10)
|
|
46
|
+
@last_update_time = current_time
|
|
47
|
+
|
|
16
48
|
render
|
|
49
|
+
puts if completed?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def set_progress(value)
|
|
53
|
+
@start_time ||= Time.now
|
|
54
|
+
@progress = [[value, 0].max, @total].min
|
|
55
|
+
|
|
56
|
+
current_time = Time.now
|
|
57
|
+
@update_history << { time: current_time, progress: @progress }
|
|
58
|
+
@update_history = @update_history.last(10)
|
|
59
|
+
@last_update_time = current_time
|
|
60
|
+
|
|
61
|
+
render
|
|
62
|
+
puts if completed?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def completed?
|
|
66
|
+
@progress >= @total
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def percentage
|
|
70
|
+
return 0 if @total == 0
|
|
71
|
+
(@progress.to_f / @total * 100).round(1)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def elapsed_time
|
|
75
|
+
return 0 unless @start_time
|
|
76
|
+
Time.now - @start_time
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def rate
|
|
80
|
+
return 0 if @update_history.length < 2
|
|
81
|
+
|
|
82
|
+
first_update = @update_history.first
|
|
83
|
+
last_update = @update_history.last
|
|
84
|
+
|
|
85
|
+
time_diff = last_update[:time] - first_update[:time]
|
|
86
|
+
progress_diff = last_update[:progress] - first_update[:progress]
|
|
87
|
+
|
|
88
|
+
return 0 if time_diff == 0
|
|
89
|
+
progress_diff / time_diff
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def eta
|
|
93
|
+
return 0 if rate == 0 || completed?
|
|
94
|
+
remaining = @total - @progress
|
|
95
|
+
remaining / rate
|
|
17
96
|
end
|
|
18
97
|
|
|
19
98
|
def render
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
99
|
+
bar_content = render_bar
|
|
100
|
+
status_text = render_status
|
|
101
|
+
|
|
102
|
+
output = ""
|
|
103
|
+
output << "\e[94m#{@title}: \e[0m" if @title
|
|
104
|
+
output << bar_content
|
|
105
|
+
output << " #{status_text}" unless status_text.empty?
|
|
106
|
+
|
|
107
|
+
print "\r\e[K#{output}"
|
|
108
|
+
$stdout.flush
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def finish(message: nil)
|
|
112
|
+
if message
|
|
113
|
+
puts "\r\e[K#{message}"
|
|
114
|
+
else
|
|
115
|
+
puts
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# 静态方法:创建带回调的进度条
|
|
120
|
+
def self.with_progress(total, **options)
|
|
121
|
+
bar = new(total, **options)
|
|
122
|
+
bar.start
|
|
123
|
+
|
|
124
|
+
begin
|
|
125
|
+
yield(bar) if block_given?
|
|
126
|
+
ensure
|
|
127
|
+
bar.finish
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
bar
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# 多进度条管理器
|
|
134
|
+
class MultiProgress
|
|
135
|
+
def initialize
|
|
136
|
+
@bars = []
|
|
137
|
+
@active = false
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def add(title, total, **options)
|
|
141
|
+
bar = ProgressBar.new(total, title: title, **options)
|
|
142
|
+
@bars << bar
|
|
143
|
+
bar
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def start
|
|
147
|
+
@active = true
|
|
148
|
+
render_all
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def render_all
|
|
152
|
+
return unless @active
|
|
153
|
+
|
|
154
|
+
print "\e[#{@bars.length}A" unless @bars.empty? # 移动光标到顶部
|
|
155
|
+
|
|
156
|
+
@bars.each_with_index do |bar, index|
|
|
157
|
+
bar_output = render_bar_line(bar)
|
|
158
|
+
puts "\e[K#{bar_output}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
$stdout.flush
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def finish_all
|
|
165
|
+
@active = false
|
|
166
|
+
puts
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
private
|
|
170
|
+
|
|
171
|
+
def render_bar_line(bar)
|
|
172
|
+
bar_content = bar.send(:render_bar)
|
|
173
|
+
status_text = bar.send(:render_status)
|
|
174
|
+
|
|
175
|
+
output = ""
|
|
176
|
+
output << "\e[94m#{bar.instance_variable_get(:@title)}: \e[0m" if bar.instance_variable_get(:@title)
|
|
177
|
+
output << bar_content
|
|
178
|
+
output << " #{status_text}" unless status_text.empty?
|
|
179
|
+
output
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
def render_bar
|
|
186
|
+
style_config = STYLES[@style] || STYLES[:default]
|
|
187
|
+
|
|
188
|
+
completed_width = (@progress.to_f / @total * @width).round
|
|
189
|
+
|
|
190
|
+
if @style == :gradient
|
|
191
|
+
filled_chars = style_config[:filled]
|
|
192
|
+
filled_full = filled_chars.last * (completed_width / filled_chars.length)
|
|
193
|
+
filled_partial = filled_chars[completed_width % filled_chars.length] if completed_width % filled_chars.length > 0
|
|
194
|
+
filled = filled_full + (filled_partial || '')
|
|
195
|
+
empty = style_config[:empty] * (@width - filled.length)
|
|
196
|
+
else
|
|
197
|
+
filled = style_config[:filled] * completed_width
|
|
198
|
+
empty = style_config[:empty] * (@width - completed_width)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# 添加颜色
|
|
202
|
+
color_filled = completed? ? "\e[92m" : "\e[96m" # 完成时绿色,进行中蓝色
|
|
203
|
+
color_empty = "\e[90m" # 空白部分灰色
|
|
204
|
+
color_reset = "\e[0m"
|
|
205
|
+
|
|
206
|
+
"#{style_config[:prefix]}#{color_filled}#{filled}#{color_empty}#{empty}#{color_reset}#{style_config[:suffix]}"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def render_status
|
|
210
|
+
parts = []
|
|
211
|
+
|
|
212
|
+
if @show_percentage
|
|
213
|
+
parts << "#{percentage}%"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
parts << "(#{@progress}/#{@total})"
|
|
217
|
+
|
|
218
|
+
if @show_rate && rate > 0
|
|
219
|
+
parts << sprintf("%.1f/s", rate)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
if @show_eta && eta > 0 && !completed?
|
|
223
|
+
eta_formatted = format_time(eta)
|
|
224
|
+
parts << "ETA: #{eta_formatted}"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
if @start_time
|
|
228
|
+
elapsed_formatted = format_time(elapsed_time)
|
|
229
|
+
parts << "#{elapsed_formatted}"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
parts.join(" ")
|
|
233
|
+
end
|
|
23
234
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
235
|
+
def format_time(seconds)
|
|
236
|
+
if seconds < 60
|
|
237
|
+
sprintf("%.1fs", seconds)
|
|
238
|
+
elsif seconds < 3600
|
|
239
|
+
minutes = seconds / 60
|
|
240
|
+
sprintf("%.1fm", minutes)
|
|
241
|
+
else
|
|
242
|
+
hours = seconds / 3600
|
|
243
|
+
sprintf("%.1fh", hours)
|
|
244
|
+
end
|
|
27
245
|
end
|
|
28
246
|
end
|
|
29
|
-
end
|
|
247
|
+
end
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
module RubyRich
|
|
2
|
+
class Status
|
|
3
|
+
# 状态指示器类型
|
|
4
|
+
INDICATORS = {
|
|
5
|
+
# 简单状态
|
|
6
|
+
success: { symbol: '✅', color: "\e[92m", text: 'Success' },
|
|
7
|
+
error: { symbol: '❌', color: "\e[91m", text: 'Error' },
|
|
8
|
+
warning: { symbol: '⚠️', color: "\e[93m", text: 'Warning' },
|
|
9
|
+
info: { symbol: 'ℹ️', color: "\e[94m", text: 'Info' },
|
|
10
|
+
|
|
11
|
+
# 进度状态
|
|
12
|
+
pending: { symbol: '⏳', color: "\e[93m", text: 'Pending' },
|
|
13
|
+
running: { symbol: '🏃', color: "\e[94m", text: 'Running' },
|
|
14
|
+
completed: { symbol: '✅', color: "\e[92m", text: 'Completed' },
|
|
15
|
+
failed: { symbol: '💥', color: "\e[91m", text: 'Failed' },
|
|
16
|
+
|
|
17
|
+
# 系统状态
|
|
18
|
+
online: { symbol: '🟢', color: "\e[92m", text: 'Online' },
|
|
19
|
+
offline: { symbol: '🔴', color: "\e[91m", text: 'Offline' },
|
|
20
|
+
maintenance: { symbol: '🔧', color: "\e[93m", text: 'Maintenance' },
|
|
21
|
+
|
|
22
|
+
# 安全状态
|
|
23
|
+
secure: { symbol: '🔒', color: "\e[92m", text: 'Secure' },
|
|
24
|
+
insecure: { symbol: '🔓', color: "\e[91m", text: 'Insecure' },
|
|
25
|
+
|
|
26
|
+
# 等级状态
|
|
27
|
+
low: { symbol: '🔵', color: "\e[94m", text: 'Low' },
|
|
28
|
+
medium: { symbol: '🟡', color: "\e[93m", text: 'Medium' },
|
|
29
|
+
high: { symbol: '🔴', color: "\e[91m", text: 'High' },
|
|
30
|
+
critical: { symbol: '💀', color: "\e[95m", text: 'Critical' }
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
# 加载动画帧
|
|
34
|
+
SPINNER_FRAMES = {
|
|
35
|
+
dots: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
|
|
36
|
+
line: ['|', '/', '-', '\\'],
|
|
37
|
+
arrow: ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙'],
|
|
38
|
+
bounce: ['⠁', '⠂', '⠄', '⠂'],
|
|
39
|
+
pulse: ['●', '◐', '◑', '◒', '◓', '◔', '◕', '○'],
|
|
40
|
+
clock: ['🕐', '🕑', '🕒', '🕓', '🕔', '🕕', '🕖', '🕗', '🕘', '🕙', '🕚', '🕛']
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
def self.indicator(type, text: nil, show_text: true, colorize: true)
|
|
44
|
+
config = INDICATORS[type.to_sym]
|
|
45
|
+
return "Unknown status: #{type}" unless config
|
|
46
|
+
|
|
47
|
+
symbol = config[:symbol]
|
|
48
|
+
color = colorize ? config[:color] : ""
|
|
49
|
+
reset = colorize ? "\e[0m" : ""
|
|
50
|
+
status_text = text || config[:text]
|
|
51
|
+
|
|
52
|
+
if show_text
|
|
53
|
+
"#{color}#{symbol} #{status_text}#{reset}"
|
|
54
|
+
else
|
|
55
|
+
"#{color}#{symbol}#{reset}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.spinner(type: :dots, text: 'Loading...', delay: 0.1)
|
|
60
|
+
frames = SPINNER_FRAMES[type.to_sym] || SPINNER_FRAMES[:dots]
|
|
61
|
+
|
|
62
|
+
Thread.new do
|
|
63
|
+
frame_index = 0
|
|
64
|
+
loop do
|
|
65
|
+
print "\r\e[K\e[94m#{frames[frame_index]}\e[0m #{text}"
|
|
66
|
+
$stdout.flush
|
|
67
|
+
sleep delay
|
|
68
|
+
frame_index = (frame_index + 1) % frames.length
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.stop_spinner(final_message: nil)
|
|
74
|
+
if final_message
|
|
75
|
+
print "\r\e[K#{final_message}\n"
|
|
76
|
+
else
|
|
77
|
+
print "\r\e[K"
|
|
78
|
+
end
|
|
79
|
+
$stdout.flush
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# 静态进度条
|
|
83
|
+
def self.progress_bar(current, total, width: 30, style: :filled)
|
|
84
|
+
percentage = (current.to_f / total * 100).round(1)
|
|
85
|
+
filled_width = (current.to_f / total * width).round
|
|
86
|
+
|
|
87
|
+
case style
|
|
88
|
+
when :filled
|
|
89
|
+
filled = '█' * filled_width
|
|
90
|
+
empty = '░' * (width - filled_width)
|
|
91
|
+
bar = "#{filled}#{empty}"
|
|
92
|
+
when :blocks
|
|
93
|
+
filled = '■' * filled_width
|
|
94
|
+
empty = '□' * (width - filled_width)
|
|
95
|
+
bar = "#{filled}#{empty}"
|
|
96
|
+
when :dots
|
|
97
|
+
filled = '●' * filled_width
|
|
98
|
+
empty = '○' * (width - filled_width)
|
|
99
|
+
bar = "#{filled}#{empty}"
|
|
100
|
+
else
|
|
101
|
+
filled = '=' * filled_width
|
|
102
|
+
empty = '-' * (width - filled_width)
|
|
103
|
+
bar = "#{filled}#{empty}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
"\e[92m[#{bar}]\e[0m #{percentage}% (#{current}/#{total})"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# 状态板
|
|
110
|
+
class StatusBoard
|
|
111
|
+
def initialize(width: 60)
|
|
112
|
+
@width = width
|
|
113
|
+
@items = []
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def add_item(label, status, description: nil)
|
|
117
|
+
@items << {
|
|
118
|
+
label: label,
|
|
119
|
+
status: status,
|
|
120
|
+
description: description
|
|
121
|
+
}
|
|
122
|
+
self
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def render(show_descriptions: true, align_status: :right)
|
|
126
|
+
lines = []
|
|
127
|
+
lines << "┌#{'─' * (@width - 2)}┐"
|
|
128
|
+
|
|
129
|
+
@items.each do |item|
|
|
130
|
+
label = item[:label]
|
|
131
|
+
status_text = RubyRich::Status.indicator(item[:status])
|
|
132
|
+
description = item[:description]
|
|
133
|
+
|
|
134
|
+
# 计算实际显示宽度(排除 ANSI 代码)
|
|
135
|
+
status_display_width = status_text.gsub(/\e\[[0-9;]*m/, '').length
|
|
136
|
+
|
|
137
|
+
case align_status
|
|
138
|
+
when :right
|
|
139
|
+
padding = @width - 4 - label.length - status_display_width
|
|
140
|
+
padding = [padding, 1].max
|
|
141
|
+
main_line = "│ #{label}#{' ' * padding}#{status_text} │"
|
|
142
|
+
when :left
|
|
143
|
+
padding = @width - 4 - label.length - status_display_width
|
|
144
|
+
padding = [padding, 1].max
|
|
145
|
+
main_line = "│ #{status_text} #{label}#{' ' * padding}│"
|
|
146
|
+
else # center
|
|
147
|
+
total_content = label.length + status_display_width + 1
|
|
148
|
+
left_padding = [(@width - 2 - total_content) / 2, 1].max
|
|
149
|
+
right_padding = @width - 2 - total_content - left_padding
|
|
150
|
+
main_line = "│#{' ' * left_padding}#{label} #{status_text}#{' ' * right_padding}│"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
lines << main_line
|
|
154
|
+
|
|
155
|
+
if show_descriptions && description
|
|
156
|
+
desc_lines = wrap_text(description, @width - 4)
|
|
157
|
+
desc_lines.each do |desc_line|
|
|
158
|
+
padding = @width - 4 - desc_line.length
|
|
159
|
+
lines << "│ \e[90m#{desc_line}#{' ' * padding}\e[0m │"
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
lines << "└#{'─' * (@width - 2)}┘"
|
|
165
|
+
lines.join("\n")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
private
|
|
169
|
+
|
|
170
|
+
def wrap_text(text, max_width)
|
|
171
|
+
words = text.split(' ')
|
|
172
|
+
lines = []
|
|
173
|
+
current_line = ''
|
|
174
|
+
|
|
175
|
+
words.each do |word|
|
|
176
|
+
if (current_line + ' ' + word).length <= max_width
|
|
177
|
+
current_line += current_line.empty? ? word : ' ' + word
|
|
178
|
+
else
|
|
179
|
+
lines << current_line unless current_line.empty?
|
|
180
|
+
current_line = word
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
lines << current_line unless current_line.empty?
|
|
185
|
+
lines
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# 实时状态监控
|
|
190
|
+
class Monitor
|
|
191
|
+
def initialize(refresh_rate: 1.0)
|
|
192
|
+
@refresh_rate = refresh_rate
|
|
193
|
+
@items = {}
|
|
194
|
+
@running = false
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def add_item(key, label, &block)
|
|
198
|
+
@items[key] = {
|
|
199
|
+
label: label,
|
|
200
|
+
block: block
|
|
201
|
+
}
|
|
202
|
+
self
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def start
|
|
206
|
+
@running = true
|
|
207
|
+
|
|
208
|
+
Thread.new do
|
|
209
|
+
while @running
|
|
210
|
+
system('clear')
|
|
211
|
+
puts render_status
|
|
212
|
+
sleep @refresh_rate
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def stop
|
|
218
|
+
@running = false
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
private
|
|
222
|
+
|
|
223
|
+
def render_status
|
|
224
|
+
lines = []
|
|
225
|
+
lines << "\e[1m\e[96mSystem Status Monitor\e[0m"
|
|
226
|
+
lines << "─" * 40
|
|
227
|
+
lines << ""
|
|
228
|
+
|
|
229
|
+
@items.each do |key, item|
|
|
230
|
+
begin
|
|
231
|
+
status = item[:block].call
|
|
232
|
+
status_indicator = RubyRich::Status.indicator(status)
|
|
233
|
+
lines << "#{item[:label]}: #{status_indicator}"
|
|
234
|
+
rescue => e
|
|
235
|
+
error_indicator = RubyRich::Status.indicator(:error, text: "Error: #{e.message}")
|
|
236
|
+
lines << "#{item[:label]}: #{error_indicator}"
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
lines << ""
|
|
241
|
+
lines << "\e[90mPress Ctrl+C to stop monitoring\e[0m"
|
|
242
|
+
lines.join("\n")
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
require 'rouge'
|
|
2
|
+
|
|
3
|
+
module RubyRich
|
|
4
|
+
class Syntax
|
|
5
|
+
# 支持的语言别名映射
|
|
6
|
+
LANGUAGE_ALIASES = {
|
|
7
|
+
'rb' => 'ruby',
|
|
8
|
+
'py' => 'python',
|
|
9
|
+
'js' => 'javascript',
|
|
10
|
+
'ts' => 'typescript',
|
|
11
|
+
'sh' => 'shell',
|
|
12
|
+
'bash' => 'shell',
|
|
13
|
+
'zsh' => 'shell',
|
|
14
|
+
'yml' => 'yaml',
|
|
15
|
+
'md' => 'markdown'
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
# 语法高亮主题颜色映射
|
|
19
|
+
THEME_COLORS = {
|
|
20
|
+
# Rouge token types to ANSI colors
|
|
21
|
+
'Comment' => "\e[90m", # Bright black (gray)
|
|
22
|
+
'Comment.Single' => "\e[90m",
|
|
23
|
+
'Comment.Multiline' => "\e[90m",
|
|
24
|
+
'Comment.Preproc' => "\e[95m", # Bright magenta
|
|
25
|
+
|
|
26
|
+
'Keyword' => "\e[94m", # Bright blue
|
|
27
|
+
'Keyword.Constant' => "\e[94m",
|
|
28
|
+
'Keyword.Declaration' => "\e[94m",
|
|
29
|
+
'Keyword.Namespace' => "\e[94m",
|
|
30
|
+
'Keyword.Pseudo' => "\e[94m",
|
|
31
|
+
'Keyword.Reserved' => "\e[94m",
|
|
32
|
+
'Keyword.Type' => "\e[94m",
|
|
33
|
+
|
|
34
|
+
'Literal' => "\e[96m", # Bright cyan
|
|
35
|
+
'Literal.Date' => "\e[96m",
|
|
36
|
+
'Literal.Number' => "\e[93m", # Bright yellow
|
|
37
|
+
'Literal.Number.Bin' => "\e[93m",
|
|
38
|
+
'Literal.Number.Float' => "\e[93m",
|
|
39
|
+
'Literal.Number.Hex' => "\e[93m",
|
|
40
|
+
'Literal.Number.Integer' => "\e[93m",
|
|
41
|
+
'Literal.Number.Oct' => "\e[93m",
|
|
42
|
+
|
|
43
|
+
'Literal.String' => "\e[92m", # Bright green
|
|
44
|
+
'Literal.String.Affix' => "\e[92m",
|
|
45
|
+
'Literal.String.Backtick' => "\e[92m",
|
|
46
|
+
'Literal.String.Char' => "\e[92m",
|
|
47
|
+
'Literal.String.Doc' => "\e[92m",
|
|
48
|
+
'Literal.String.Double' => "\e[92m",
|
|
49
|
+
'Literal.String.Escape' => "\e[96m",
|
|
50
|
+
'Literal.String.Heredoc' => "\e[92m",
|
|
51
|
+
'Literal.String.Interpol' => "\e[96m",
|
|
52
|
+
'Literal.String.Other' => "\e[92m",
|
|
53
|
+
'Literal.String.Regex' => "\e[91m", # Bright red
|
|
54
|
+
'Literal.String.Single' => "\e[92m",
|
|
55
|
+
'Literal.String.Symbol' => "\e[95m", # Bright magenta
|
|
56
|
+
|
|
57
|
+
'Name' => "\e[97m", # Bright white
|
|
58
|
+
'Name.Attribute' => "\e[93m", # Bright yellow
|
|
59
|
+
'Name.Builtin' => "\e[96m", # Bright cyan
|
|
60
|
+
'Name.Builtin.Pseudo' => "\e[96m",
|
|
61
|
+
'Name.Class' => "\e[93m", # Bright yellow
|
|
62
|
+
'Name.Constant' => "\e[93m",
|
|
63
|
+
'Name.Decorator' => "\e[95m", # Bright magenta
|
|
64
|
+
'Name.Entity' => "\e[93m",
|
|
65
|
+
'Name.Exception' => "\e[91m", # Bright red
|
|
66
|
+
'Name.Function' => "\e[96m", # Bright cyan
|
|
67
|
+
'Name.Property' => "\e[96m",
|
|
68
|
+
'Name.Label' => "\e[95m",
|
|
69
|
+
'Name.Namespace' => "\e[93m",
|
|
70
|
+
'Name.Other' => "\e[97m",
|
|
71
|
+
'Name.Tag' => "\e[94m", # Bright blue
|
|
72
|
+
'Name.Variable' => "\e[97m",
|
|
73
|
+
'Name.Variable.Class' => "\e[97m",
|
|
74
|
+
'Name.Variable.Global' => "\e[97m",
|
|
75
|
+
'Name.Variable.Instance' => "\e[97m",
|
|
76
|
+
|
|
77
|
+
'Operator' => "\e[91m", # Bright red
|
|
78
|
+
'Operator.Word' => "\e[94m", # Bright blue
|
|
79
|
+
|
|
80
|
+
'Punctuation' => "\e[97m", # Bright white
|
|
81
|
+
|
|
82
|
+
'Error' => "\e[101m\e[97m", # Red background, white text
|
|
83
|
+
'Generic.Deleted' => "\e[91m", # Bright red
|
|
84
|
+
'Generic.Emph' => "\e[3m", # Italic
|
|
85
|
+
'Generic.Error' => "\e[91m", # Bright red
|
|
86
|
+
'Generic.Heading' => "\e[1m\e[94m", # Bold bright blue
|
|
87
|
+
'Generic.Inserted' => "\e[92m", # Bright green
|
|
88
|
+
'Generic.Output' => "\e[90m", # Bright black (gray)
|
|
89
|
+
'Generic.Prompt' => "\e[1m", # Bold
|
|
90
|
+
'Generic.Strong' => "\e[1m", # Bold
|
|
91
|
+
'Generic.Subheading' => "\e[95m", # Bright magenta
|
|
92
|
+
'Generic.Traceback' => "\e[91m" # Bright red
|
|
93
|
+
}.freeze
|
|
94
|
+
|
|
95
|
+
def self.highlight(code, language = nil, theme: :default)
|
|
96
|
+
new(theme: theme).highlight(code, language)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def initialize(theme: :default)
|
|
100
|
+
@theme = theme
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def highlight(code, language = nil)
|
|
104
|
+
return code if code.nil? || code.empty?
|
|
105
|
+
|
|
106
|
+
# 检测或规范化语言
|
|
107
|
+
language = detect_language(code) if language.nil?
|
|
108
|
+
language = normalize_language(language) if language
|
|
109
|
+
|
|
110
|
+
# 如果无法检测语言,返回原始代码
|
|
111
|
+
return code unless language
|
|
112
|
+
|
|
113
|
+
begin
|
|
114
|
+
lexer = Rouge::Lexer.find(language)
|
|
115
|
+
return code unless lexer
|
|
116
|
+
|
|
117
|
+
# 使用自定义格式化器
|
|
118
|
+
formatter = AnsiFormatter.new
|
|
119
|
+
formatter.format(lexer.lex(code))
|
|
120
|
+
rescue => e
|
|
121
|
+
# 如果高亮失败,返回原始代码
|
|
122
|
+
code
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def list_languages
|
|
127
|
+
Rouge::Lexer.all.map(&:tag).sort
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def detect_language(code)
|
|
133
|
+
# 简单的语言检测启发式
|
|
134
|
+
return 'ruby' if code.include?('def ') && code.include?('end')
|
|
135
|
+
return 'python' if code.include?('def ') && code.include?(':')
|
|
136
|
+
return 'javascript' if code.include?('function') || code.include?('=>')
|
|
137
|
+
return 'html' if code.include?('<html') || code.include?('<!DOCTYPE')
|
|
138
|
+
return 'css' if code.match?(/\w+\s*{[^}]*}/)
|
|
139
|
+
return 'shell' if code.start_with?('#!/bin/') || code.include?('$ ')
|
|
140
|
+
return 'json' if code.strip.start_with?('{') && code.include?(':')
|
|
141
|
+
return 'yaml' if code.include?('---') || code.match?(/^\w+:/)
|
|
142
|
+
|
|
143
|
+
nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def normalize_language(language)
|
|
147
|
+
return nil if language.nil?
|
|
148
|
+
language = language.to_s.downcase.strip
|
|
149
|
+
LANGUAGE_ALIASES[language] || language
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# 自定义 ANSI 格式化器
|
|
153
|
+
class AnsiFormatter
|
|
154
|
+
def format(tokens)
|
|
155
|
+
output = ''
|
|
156
|
+
tokens.each do |token, value|
|
|
157
|
+
color_code = THEME_COLORS[token.qualname] ||
|
|
158
|
+
THEME_COLORS[token.token_chain.map(&:qualname).find { |t| THEME_COLORS[t] }] ||
|
|
159
|
+
''
|
|
160
|
+
|
|
161
|
+
if color_code.empty?
|
|
162
|
+
output << value
|
|
163
|
+
else
|
|
164
|
+
output << "#{color_code}#{value}\e[0m"
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
output
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|