ruby_rich 0.1.0 → 0.2.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/console.rb +100 -5
- data/lib/ruby_rich/dialog.rb +136 -0
- data/lib/ruby_rich/layout.rb +225 -0
- data/lib/ruby_rich/live.rb +101 -0
- data/lib/ruby_rich/{rich_panel.rb → panel.rb} +112 -0
- data/lib/ruby_rich/progress_bar.rb +3 -0
- data/lib/ruby_rich/version.rb +2 -2
- data/lib/ruby_rich.rb +6 -3
- metadata +8 -5
- /data/lib/ruby_rich/{rich_print.rb → print.rb} +0 -0
- /data/lib/ruby_rich/{rich_text.rb → text.rb} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 22f7fb9cb3aa52b99809ca646ca234aa1f098967a8cfd13caddb49d71b3e455a
|
4
|
+
data.tar.gz: adbd0feae9e0a6bb2f4b631b390a1a067b6426ab99fb94ce19c4f0b3923c55e3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: abd1b8ecbcc18f755cd72182f59774fb2a28dcf763610c2486ba3f80f00671ee590a6c132692d1ad981d7d01fa66964204ac904ee710ac801ca03c7ccf3c9486
|
7
|
+
data.tar.gz: 3cd9279be0f8a2f47a451263ca6813ac1a6ba46490dc4db829f52336307e17edd0a8c3eefdd816ee79d07cb61d91b2ba6088cfe7f0eec59673f0f3dbebffbe96
|
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,86 @@ 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
|
+
|
80
|
+
# 优先处理回车键(ASCII 13 = \r,ASCII 10 = \n)
|
81
|
+
if char == "\r" || char == "\n"
|
82
|
+
return :enter
|
83
|
+
end
|
84
|
+
# 单独处理 Tab 键(ASCII 9)
|
85
|
+
if char == "\t"
|
86
|
+
return :tab
|
87
|
+
elsif char.ord == 0x07F
|
88
|
+
return :backspace
|
89
|
+
elsif char == "\e" # 检测到转义序列
|
90
|
+
sequence = ''
|
91
|
+
begin
|
92
|
+
while (c = io.read_nonblock(1))
|
93
|
+
sequence << c
|
94
|
+
end
|
95
|
+
rescue IO::WaitReadable
|
96
|
+
retry if IO.select([io], nil, nil, 0.01)
|
97
|
+
rescue EOFError
|
98
|
+
end
|
99
|
+
return ESCAPE_SEQUENCES[sequence] || :escape
|
100
|
+
# 处理 Ctrl 组合键(排除 Tab 和回车)
|
101
|
+
elsif char.ord.between?(1, 8) || char.ord.between?(10, 26)
|
102
|
+
ctrl_char = (char.ord + 64).chr.downcase
|
103
|
+
return :"ctrl_#{ctrl_char}"
|
104
|
+
else
|
105
|
+
char
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
14
110
|
def add_line(text)
|
15
111
|
@lines << text
|
16
112
|
end
|
17
113
|
|
18
114
|
def clear
|
19
|
-
print "\e[H\e[2J"
|
115
|
+
Kernel.print "\e[H\e[2J"
|
20
116
|
end
|
21
117
|
|
22
118
|
def render
|
@@ -24,14 +120,13 @@ module RubyRich
|
|
24
120
|
@lines.each_with_index do |line, index|
|
25
121
|
formatted_line = format_line(line)
|
26
122
|
@buffer << formatted_line
|
27
|
-
puts formatted_line
|
28
|
-
puts "\n" * @layout[:spacing] if index < @lines.size - 1
|
123
|
+
Kernel.puts formatted_line
|
124
|
+
Kernel.puts "\n" * @layout[:spacing] if index < @lines.size - 1
|
29
125
|
end
|
30
126
|
end
|
31
127
|
|
32
128
|
def update_line(index, text)
|
33
129
|
return unless index.between?(0, @lines.size - 1)
|
34
|
-
|
35
130
|
@lines[index] = text
|
36
131
|
render
|
37
132
|
end
|
@@ -50,4 +145,4 @@ module RubyRich
|
|
50
145
|
end
|
51
146
|
end
|
52
147
|
end
|
53
|
-
end
|
148
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
module RubyRich
|
2
|
+
class Dialog
|
3
|
+
attr_accessor :title, :content, :buttons
|
4
|
+
|
5
|
+
def initialize(title: "", content: "", buttons: ["OK"])
|
6
|
+
@title = title
|
7
|
+
@content = content
|
8
|
+
@buttons = buttons
|
9
|
+
@selected_index = 0
|
10
|
+
@result = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
# 主显示方法
|
14
|
+
def show
|
15
|
+
Console.raw do
|
16
|
+
loop do
|
17
|
+
Console.clear
|
18
|
+
render
|
19
|
+
handle_input(Console.get_key)
|
20
|
+
break if @result
|
21
|
+
end
|
22
|
+
end
|
23
|
+
@result
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# 渲染对话框
|
29
|
+
def render
|
30
|
+
build_layout unless @layout
|
31
|
+
update_panels
|
32
|
+
@layout.draw
|
33
|
+
end
|
34
|
+
|
35
|
+
def build_layout
|
36
|
+
@layout = RubyRich::Layout.new
|
37
|
+
@layout.split_column(
|
38
|
+
RubyRich::Layout.new(size: 1, name: :title_area), # 标题区域
|
39
|
+
RubyRich::Layout.new(name: :content_area), # 内容区域
|
40
|
+
RubyRich::Layout.new(size: 3, name: :button_area) # 按钮区域
|
41
|
+
)
|
42
|
+
|
43
|
+
@title_panel = RubyRich::Panel.new("", title: @title, border_style: :white)
|
44
|
+
@content_panel = RubyRich::Panel.new("", border_style: :default)
|
45
|
+
@button_panel = RubyRich::Panel.new("", border_style: :blue)
|
46
|
+
|
47
|
+
@layout[:title_area].update_content(@title_panel)
|
48
|
+
@layout[:content_area].update_content(@content_panel)
|
49
|
+
@layout[:button_area].update_content(@button_panel)
|
50
|
+
end
|
51
|
+
|
52
|
+
def update_panels
|
53
|
+
# 更新内容面板
|
54
|
+
@content_panel.content = @content.lines.map { |line|
|
55
|
+
line.chomp.gsub(/\n/, ' ').scan(/.{1,#{@content_panel.inner_width}}/)
|
56
|
+
}.flatten.join("\n")
|
57
|
+
|
58
|
+
# 更新按钮面板
|
59
|
+
button_row = @buttons.each_with_index.map { |btn, i|
|
60
|
+
i == @selected_index ? "[#{btn}]" : " #{btn} "
|
61
|
+
}.join(" ")
|
62
|
+
|
63
|
+
@button_panel.content = button_row.center(@button_panel.inner_width)
|
64
|
+
end
|
65
|
+
|
66
|
+
# 渲染按钮区域
|
67
|
+
def render_buttons(right_x, bottom_y)
|
68
|
+
# 已通过布局系统处理按钮渲染
|
69
|
+
end
|
70
|
+
|
71
|
+
# 处理键盘输入
|
72
|
+
def handle_input(key)
|
73
|
+
case key
|
74
|
+
when :left then @selected_index = (@selected_index - 1) % @buttons.size
|
75
|
+
when :right then @selected_index = (@selected_index + 1) % @buttons.size
|
76
|
+
when :enter then @result = @buttons[@selected_index]
|
77
|
+
when :q, "\u0003" then @result = :cancel # Ctrl+C
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# 确认对话框
|
83
|
+
class ConfirmDialog < Dialog
|
84
|
+
def initialize(title: "确认", content: "确定要执行此操作吗?")
|
85
|
+
super(
|
86
|
+
title: title,
|
87
|
+
content: content,
|
88
|
+
buttons: ["取消", "确定"]
|
89
|
+
)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# 输入对话框
|
94
|
+
class InputDialog < Dialog
|
95
|
+
def initialize(title: "输入", prompt: "请输入:")
|
96
|
+
super(title: title, buttons: ["确定"])
|
97
|
+
@input = ""
|
98
|
+
@prompt = prompt
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def build_layout
|
104
|
+
super
|
105
|
+
# 在内容区域下方添加输入行
|
106
|
+
@layout[:content_area].split_column(
|
107
|
+
RubyRich::Layout.new(ratio: 1, name: :main_content),
|
108
|
+
RubyRich::Layout.new(size: 3, name: :input_line)
|
109
|
+
)
|
110
|
+
|
111
|
+
@input_panel = RubyRich::Panel.new("", border_style: :yellow)
|
112
|
+
@layout[:input_line].update_content(@input_panel)
|
113
|
+
end
|
114
|
+
|
115
|
+
def update_panels
|
116
|
+
super
|
117
|
+
@input_panel.content = "#{@prompt}#{@input}"
|
118
|
+
end
|
119
|
+
|
120
|
+
def handle_input(key)
|
121
|
+
case key
|
122
|
+
when String
|
123
|
+
@input << key
|
124
|
+
when :backspace
|
125
|
+
@input.chop!
|
126
|
+
else
|
127
|
+
super
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def show
|
132
|
+
super
|
133
|
+
@result == "确定" ? @input : nil
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,225 @@
|
|
1
|
+
module RubyRich
|
2
|
+
class Layout
|
3
|
+
attr_accessor :name, :ratio, :size, :children, :content, :parent, :x_offset, :y_offset, :width, :height
|
4
|
+
attr_reader :split_direction
|
5
|
+
|
6
|
+
def initialize(name: nil, ratio: 1, size: nil)
|
7
|
+
@name = name
|
8
|
+
@ratio = ratio
|
9
|
+
@size = size
|
10
|
+
@children = []
|
11
|
+
@content = nil
|
12
|
+
@parent = nil
|
13
|
+
@x_offset = 0
|
14
|
+
@y_offset = 0
|
15
|
+
@width = nil
|
16
|
+
@height = nil
|
17
|
+
@split_direction = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def root
|
21
|
+
@parent ? @parent.root : self
|
22
|
+
end
|
23
|
+
|
24
|
+
def split_row(*layouts)
|
25
|
+
@split_direction = :row
|
26
|
+
layouts.each { |l| l.parent = self }
|
27
|
+
@children.concat(layouts)
|
28
|
+
end
|
29
|
+
|
30
|
+
def split_column(*layouts)
|
31
|
+
@split_direction = :column
|
32
|
+
layouts.each { |l| l.parent = self }
|
33
|
+
@children.concat(layouts)
|
34
|
+
end
|
35
|
+
|
36
|
+
def update_content(content)
|
37
|
+
@content = content
|
38
|
+
end
|
39
|
+
|
40
|
+
def calculate_dimensions(terminal_width, terminal_height)
|
41
|
+
@x_offset = 0
|
42
|
+
@y_offset = 0
|
43
|
+
calculate_node_dimensions(terminal_width, terminal_height)
|
44
|
+
end
|
45
|
+
|
46
|
+
def [](name)
|
47
|
+
find_by_name(name)
|
48
|
+
end
|
49
|
+
|
50
|
+
def find_by_name(name)
|
51
|
+
return self if @name == name
|
52
|
+
@children.each do |child|
|
53
|
+
result = child.find_by_name(name)
|
54
|
+
return result if result
|
55
|
+
end
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
|
59
|
+
def render
|
60
|
+
# 将缓冲区转换为字符串(每行用换行符连接)
|
61
|
+
render_to_buffer.map { |line| line.compact.join("") }.join("\n")
|
62
|
+
end
|
63
|
+
|
64
|
+
def render_to_buffer
|
65
|
+
# 初始化缓冲区(二维数组,每个元素代表一个字符)
|
66
|
+
buffer = Array.new(@height) { Array.new(@width, " ") }
|
67
|
+
|
68
|
+
# 递归填充内容到缓冲区
|
69
|
+
render_into(buffer)
|
70
|
+
|
71
|
+
return buffer
|
72
|
+
end
|
73
|
+
|
74
|
+
def draw
|
75
|
+
puts render
|
76
|
+
end
|
77
|
+
|
78
|
+
def render_into(buffer)
|
79
|
+
children.each { |child| child.render_into(buffer) } if children
|
80
|
+
return unless content
|
81
|
+
content_lines = if content.is_a?(String)
|
82
|
+
content.split("\n")[0...height]
|
83
|
+
else
|
84
|
+
content.render[0...height]
|
85
|
+
end
|
86
|
+
|
87
|
+
content_lines.each_with_index do |line, line_index|
|
88
|
+
y_pos = y_offset + line_index
|
89
|
+
next if y_pos >= buffer.size
|
90
|
+
|
91
|
+
in_escape = false
|
92
|
+
escape_char = ""
|
93
|
+
char_width = 0 # 初始宽度调整为0,方便位置计算
|
94
|
+
line.each_char do |char|
|
95
|
+
# 处理ANSI转义码
|
96
|
+
if in_escape
|
97
|
+
escape_char += char
|
98
|
+
in_escape = false if char == 'm'
|
99
|
+
if escape_char=="\e[0m"
|
100
|
+
escape_char = ""
|
101
|
+
end
|
102
|
+
next
|
103
|
+
elsif char.ord == 27 # 检测到转义开始符\e
|
104
|
+
in_escape = true
|
105
|
+
escape_char += char
|
106
|
+
next
|
107
|
+
end
|
108
|
+
|
109
|
+
# 计算字符宽度
|
110
|
+
char_w = case char.ord
|
111
|
+
when 0x0000..0x007F then 1 # 英文字符
|
112
|
+
when 0x4E00..0x9FFF then 2 # 中文字符
|
113
|
+
else Unicode::DisplayWidth.of(char)
|
114
|
+
end
|
115
|
+
# 计算字符的起始位置
|
116
|
+
x_start = x_offset + char_width
|
117
|
+
|
118
|
+
# 超出右边界则跳过
|
119
|
+
next if x_start >= buffer[y_pos].size
|
120
|
+
|
121
|
+
# 处理字符渲染(中文字符可能占用多个位置)
|
122
|
+
char_w.times do |i|
|
123
|
+
x_pos = x_start + i
|
124
|
+
break if x_pos >= buffer[y_pos].size # 超出右边界停止
|
125
|
+
unless escape_char.empty?
|
126
|
+
char = escape_char + char + "\e[0m" # 每次都记录字符的实际颜色
|
127
|
+
end
|
128
|
+
buffer[y_pos][x_pos] = char unless i > 0 # 中文字符仅在第一个位置写入,避免覆盖
|
129
|
+
buffer[y_pos][x_pos+1] = nil if char_w == 2
|
130
|
+
end
|
131
|
+
char_width += char_w # 更新累计宽度
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def calculate_node_dimensions(available_width, available_height)
|
137
|
+
# 只在未设置宽度时计算宽度
|
138
|
+
@width ||= if @size
|
139
|
+
[@size, available_width].min
|
140
|
+
else
|
141
|
+
available_width
|
142
|
+
end
|
143
|
+
|
144
|
+
# 只在未设置高度时计算高度
|
145
|
+
@height ||= if @size
|
146
|
+
[@size, available_height].min
|
147
|
+
else
|
148
|
+
available_height
|
149
|
+
end
|
150
|
+
|
151
|
+
if @content.class == RubyRich::Panel
|
152
|
+
@content.width = @width
|
153
|
+
@content.height = @height
|
154
|
+
end
|
155
|
+
|
156
|
+
return if @children.empty?
|
157
|
+
|
158
|
+
case @split_direction
|
159
|
+
when :row
|
160
|
+
remaining_width = @width
|
161
|
+
fixed_children, flexible_children = @children.partition { |c| c.size }
|
162
|
+
|
163
|
+
fixed_children.each do |child|
|
164
|
+
child_width = [child.size, remaining_width].min
|
165
|
+
child.width = child_width
|
166
|
+
remaining_width -= child_width
|
167
|
+
end
|
168
|
+
|
169
|
+
total_ratio = flexible_children.sum(&:ratio)
|
170
|
+
if total_ratio > 0
|
171
|
+
ratio_width = remaining_width.to_f / total_ratio
|
172
|
+
flexible_children.each do |child|
|
173
|
+
child_width = (child.ratio * ratio_width).floor
|
174
|
+
child.width = child_width
|
175
|
+
remaining_width -= child_width
|
176
|
+
end
|
177
|
+
|
178
|
+
flexible_children.last.width += remaining_width if remaining_width > 0
|
179
|
+
end
|
180
|
+
|
181
|
+
@children.each { |child| child.height = @height }
|
182
|
+
|
183
|
+
current_x = @x_offset
|
184
|
+
@children.each do |child|
|
185
|
+
child.x_offset = current_x
|
186
|
+
child.y_offset = @y_offset
|
187
|
+
current_x += child.width
|
188
|
+
child.calculate_node_dimensions(child.width, child.height)
|
189
|
+
end
|
190
|
+
|
191
|
+
when :column
|
192
|
+
remaining_height = @height
|
193
|
+
fixed_children, flexible_children = @children.partition { |c| c.size }
|
194
|
+
|
195
|
+
fixed_children.each do |child|
|
196
|
+
child_height = [child.size, remaining_height].min
|
197
|
+
child.height = child_height
|
198
|
+
remaining_height -= child_height
|
199
|
+
end
|
200
|
+
|
201
|
+
total_ratio = flexible_children.sum(&:ratio)
|
202
|
+
if total_ratio > 0
|
203
|
+
ratio_height = remaining_height.to_f / total_ratio
|
204
|
+
flexible_children.each do |child|
|
205
|
+
child_height = (child.ratio * ratio_height).floor
|
206
|
+
child.height = child_height
|
207
|
+
remaining_height -= child_height
|
208
|
+
end
|
209
|
+
|
210
|
+
flexible_children.last.height += remaining_height if remaining_height > 0
|
211
|
+
end
|
212
|
+
|
213
|
+
@children.each { |child| child.width = @width }
|
214
|
+
|
215
|
+
current_y = @y_offset
|
216
|
+
@children.each do |child|
|
217
|
+
child.x_offset = @x_offset
|
218
|
+
child.y_offset = current_y
|
219
|
+
current_y += child.height
|
220
|
+
child.calculate_node_dimensions(child.width, child.height)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
@@ -0,0 +1,101 @@
|
|
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
|
+
class << self
|
38
|
+
def start(layout, refresh_rate: 30, full_screen: false, &proc)
|
39
|
+
setup_terminal
|
40
|
+
live = new(layout, refresh_rate)
|
41
|
+
live.run(proc)
|
42
|
+
rescue => e
|
43
|
+
puts e.message
|
44
|
+
ensure
|
45
|
+
restore_terminal
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def setup_terminal
|
51
|
+
@original_state = `stty -g`
|
52
|
+
system("stty -echo")
|
53
|
+
end
|
54
|
+
|
55
|
+
def restore_terminal
|
56
|
+
system("stty #{@original_state}")
|
57
|
+
print TTY::Cursor.show
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def initialize(layout, refresh_rate)
|
62
|
+
@layout = layout
|
63
|
+
@refresh_rate = refresh_rate
|
64
|
+
@running = true
|
65
|
+
@last_frame = Time.now
|
66
|
+
@cursor = TTY::Cursor
|
67
|
+
@render = CacheRender.new
|
68
|
+
end
|
69
|
+
|
70
|
+
def run(proc = nil)
|
71
|
+
while @running
|
72
|
+
render_frame
|
73
|
+
proc.call(self) if proc
|
74
|
+
sleep 1.0 / @refresh_rate
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def stop
|
79
|
+
@running = false
|
80
|
+
end
|
81
|
+
|
82
|
+
def move_cursor(x,y)
|
83
|
+
print @cursor.move_to(x, y)
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def render_frame
|
89
|
+
@layout.calculate_dimensions(terminal_width, terminal_height)
|
90
|
+
@render.draw(@layout.render_to_buffer)
|
91
|
+
end
|
92
|
+
|
93
|
+
def terminal_width
|
94
|
+
TTY::Screen.width
|
95
|
+
end
|
96
|
+
|
97
|
+
def terminal_height
|
98
|
+
TTY::Screen.height
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -1,3 +1,115 @@
|
|
1
|
+
module RubyRich
|
2
|
+
class Panel
|
3
|
+
attr_accessor :width, :height, :content, :line_pos, :border_style, :title
|
4
|
+
COLORS = {
|
5
|
+
red: "\e[31m",
|
6
|
+
green: "\e[32m",
|
7
|
+
blue: "\e[34m",
|
8
|
+
yellow: "\e[33m",
|
9
|
+
cyan: "\e[36m",
|
10
|
+
white: "\e[37m",
|
11
|
+
reset: "\e[0m"
|
12
|
+
}
|
13
|
+
|
14
|
+
def initialize(content = "", title: nil, border_style: :white)
|
15
|
+
@content = content
|
16
|
+
@title = title
|
17
|
+
@border_style = border_style
|
18
|
+
@width = 0
|
19
|
+
@height = 0
|
20
|
+
@line_pos = 0
|
21
|
+
end
|
22
|
+
|
23
|
+
def inner_width
|
24
|
+
@width - 2 # Account for border characters
|
25
|
+
end
|
26
|
+
|
27
|
+
def calculate_string_width(str)
|
28
|
+
width = 0
|
29
|
+
str.each_char do |char|
|
30
|
+
width += Unicode::DisplayWidth.of(char)
|
31
|
+
end
|
32
|
+
width
|
33
|
+
end
|
34
|
+
|
35
|
+
def render
|
36
|
+
lines = []
|
37
|
+
color_code = COLORS[@border_style] || COLORS[:white]
|
38
|
+
reset_code = COLORS[:reset]
|
39
|
+
|
40
|
+
# Top border
|
41
|
+
top_border = color_code + "┌"
|
42
|
+
if @title
|
43
|
+
title_text = "[ #{@title} ]"
|
44
|
+
top_border += title_text + '─' * (@width - calculate_string_width(@title)-6)
|
45
|
+
else
|
46
|
+
top_border += '─' * (@width - 2)
|
47
|
+
end
|
48
|
+
top_border += "┐" + reset_code
|
49
|
+
lines << top_border
|
50
|
+
|
51
|
+
# Content area
|
52
|
+
content_lines = wrap_content(@content)
|
53
|
+
if content_lines.size > @height - 2
|
54
|
+
@line_pos = content_lines.size - @height + 2
|
55
|
+
content_lines=content_lines[@line_pos..-1]
|
56
|
+
end
|
57
|
+
content_lines.each do |line|
|
58
|
+
lines << color_code + "│" + reset_code +
|
59
|
+
line + " "*(@width - calculate_string_width(line) - 2) +
|
60
|
+
color_code + "│" + reset_code
|
61
|
+
end
|
62
|
+
|
63
|
+
# Fill remaining vertical space
|
64
|
+
remaining_lines = @height - 2 - content_lines.size
|
65
|
+
remaining_lines.times do
|
66
|
+
lines << color_code + "│" + reset_code +
|
67
|
+
" " * (@width - 2) +
|
68
|
+
color_code + "│" + reset_code
|
69
|
+
end
|
70
|
+
|
71
|
+
# Bottom border
|
72
|
+
lines << color_code + "└" + "─" * (@width - 2) + "┘" + reset_code
|
73
|
+
|
74
|
+
lines
|
75
|
+
end
|
76
|
+
|
77
|
+
def update_content(new_content)
|
78
|
+
@content = new_content
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def split_text_by_width(text)
|
84
|
+
result = []
|
85
|
+
current_line = ""
|
86
|
+
current_width = 0
|
87
|
+
|
88
|
+
text.each_char do |char|
|
89
|
+
char_width = Unicode::DisplayWidth.of(char)
|
90
|
+
if current_width + char_width <= @width - 4
|
91
|
+
current_line += char
|
92
|
+
current_width += char_width
|
93
|
+
else
|
94
|
+
result << current_line
|
95
|
+
current_line = char
|
96
|
+
current_width = char_width
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# 添加最后一行
|
101
|
+
result << current_line unless current_line.empty?
|
102
|
+
|
103
|
+
result
|
104
|
+
end
|
105
|
+
|
106
|
+
def wrap_content(text)
|
107
|
+
text.split("\n").flat_map do |line|
|
108
|
+
split_text_by_width(line)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
1
113
|
module RubyRich
|
2
114
|
class RichPanel
|
3
115
|
# ANSI escape codes for styling
|
data/lib/ruby_rich/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
1
|
module RubyRich
|
2
|
-
VERSION = "0.
|
3
|
-
end
|
2
|
+
VERSION = "0.2.0"
|
3
|
+
end
|
data/lib/ruby_rich.rb
CHANGED
@@ -10,9 +10,12 @@ 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'
|
16
19
|
require_relative 'ruby_rich/version'
|
17
20
|
|
18
21
|
# 定义主模块
|
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.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- zhuang biaowei
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-02-
|
10
|
+
date: 2025-02-10 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: rake
|
@@ -46,11 +46,14 @@ extra_rdoc_files: []
|
|
46
46
|
files:
|
47
47
|
- lib/ruby_rich.rb
|
48
48
|
- lib/ruby_rich/console.rb
|
49
|
+
- lib/ruby_rich/dialog.rb
|
50
|
+
- lib/ruby_rich/layout.rb
|
51
|
+
- lib/ruby_rich/live.rb
|
52
|
+
- lib/ruby_rich/panel.rb
|
53
|
+
- lib/ruby_rich/print.rb
|
49
54
|
- 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
55
|
- lib/ruby_rich/table.rb
|
56
|
+
- lib/ruby_rich/text.rb
|
54
57
|
- lib/ruby_rich/version.rb
|
55
58
|
homepage: https://github.com/zhuangbiaowei/ruby_rich
|
56
59
|
licenses:
|
File without changes
|
File without changes
|