ruby_rich 0.2.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 +11 -8
- data/lib/ruby_rich/dialog.rb +46 -116
- data/lib/ruby_rich/layout.rb +76 -7
- data/lib/ruby_rich/live.rb +21 -4
- data/lib/ruby_rich/panel.rb +100 -123
- data/lib/ruby_rich/print.rb +1 -1
- data/lib/ruby_rich/table.rb +1 -1
- data/lib/ruby_rich/text.rb +13 -37
- data/lib/ruby_rich/version.rb +1 -1
- data/lib/ruby_rich.rb +1 -0
- metadata +4 -3
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
@@ -18,7 +18,7 @@ module RubyRich
|
|
18
18
|
# 其他
|
19
19
|
'[5~' => :page_up, '[6~' => :page_down,
|
20
20
|
'[H' => :home, '[F' => :end,
|
21
|
-
'[2~' => :insert, '[3~' => :delete
|
21
|
+
'[2~' => :insert, '[3~' => :delete
|
22
22
|
}.freeze
|
23
23
|
|
24
24
|
def initialize
|
@@ -76,16 +76,15 @@ module RubyRich
|
|
76
76
|
def get_key(input: $stdin)
|
77
77
|
input.raw(intr: true) do |io|
|
78
78
|
char = io.getch
|
79
|
-
|
80
79
|
# 优先处理回车键(ASCII 13 = \r,ASCII 10 = \n)
|
81
80
|
if char == "\r" || char == "\n"
|
82
|
-
return :enter
|
81
|
+
return {:name=>:enter}
|
83
82
|
end
|
84
83
|
# 单独处理 Tab 键(ASCII 9)
|
85
84
|
if char == "\t"
|
86
|
-
return :tab
|
85
|
+
return {:name=>:tab}
|
87
86
|
elsif char.ord == 0x07F
|
88
|
-
return :backspace
|
87
|
+
return {:name=>:backspace}
|
89
88
|
elsif char == "\e" # 检测到转义序列
|
90
89
|
sequence = ''
|
91
90
|
begin
|
@@ -96,13 +95,17 @@ module RubyRich
|
|
96
95
|
retry if IO.select([io], nil, nil, 0.01)
|
97
96
|
rescue EOFError
|
98
97
|
end
|
99
|
-
|
98
|
+
if sequence.empty?
|
99
|
+
return {:name => :escape}
|
100
|
+
else
|
101
|
+
return {:name => ESCAPE_SEQUENCES[sequence]} || {:name => :escape}
|
102
|
+
end
|
100
103
|
# 处理 Ctrl 组合键(排除 Tab 和回车)
|
101
104
|
elsif char.ord.between?(1, 8) || char.ord.between?(10, 26)
|
102
105
|
ctrl_char = (char.ord + 64).chr.downcase
|
103
|
-
return :"ctrl_#{ctrl_char}"
|
106
|
+
return {:name =>"ctrl_#{ctrl_char}".to_sym}
|
104
107
|
else
|
105
|
-
char
|
108
|
+
{:name => :string, :value => char}
|
106
109
|
end
|
107
110
|
end
|
108
111
|
end
|
data/lib/ruby_rich/dialog.rb
CHANGED
@@ -1,136 +1,66 @@
|
|
1
1
|
module RubyRich
|
2
2
|
class Dialog
|
3
|
-
attr_accessor :title, :content, :buttons
|
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
|
4
19
|
|
5
|
-
def
|
6
|
-
@
|
7
|
-
@content = content
|
8
|
-
@buttons = buttons
|
9
|
-
@selected_index = 0
|
10
|
-
@result = nil
|
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
|
11
22
|
end
|
12
23
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
21
32
|
end
|
22
33
|
end
|
23
|
-
|
34
|
+
str.strip
|
24
35
|
end
|
25
36
|
|
26
|
-
|
27
|
-
|
28
|
-
# 渲染对话框
|
29
|
-
def render
|
30
|
-
build_layout unless @layout
|
31
|
-
update_panels
|
32
|
-
@layout.draw
|
37
|
+
def render_to_buffer
|
38
|
+
@layout.render_to_buffer
|
33
39
|
end
|
34
40
|
|
35
|
-
def
|
36
|
-
@
|
37
|
-
|
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
|
41
|
+
def key(event_name, priority = 0, &block)
|
42
|
+
unless @event_listeners[event_name]
|
43
|
+
@event_listeners[event_name] = []
|
78
44
|
end
|
45
|
+
@event_listeners[event_name] << { priority: priority, block: block }
|
46
|
+
@event_listeners[event_name].sort_by! { |l| -l[:priority] } # Higher priority first
|
79
47
|
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
48
|
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
49
|
+
def on(event_name, &block)
|
50
|
+
if event_name==:close
|
51
|
+
@event_listeners[event_name] = [{block: block}]
|
128
52
|
end
|
129
53
|
end
|
130
54
|
|
131
|
-
def
|
132
|
-
|
133
|
-
|
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
|
134
64
|
end
|
135
65
|
end
|
136
66
|
end
|
data/lib/ruby_rich/layout.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
module RubyRich
|
2
2
|
class Layout
|
3
|
-
attr_accessor :name, :ratio, :size, :children, :content, :parent, :
|
3
|
+
attr_accessor :name, :ratio, :size, :children, :content, :parent, :live, :dialog
|
4
|
+
attr_accessor :x_offset, :y_offset, :width, :height, :show
|
4
5
|
attr_reader :split_direction
|
5
6
|
|
6
|
-
def initialize(name: nil, ratio: 1, size: nil)
|
7
|
+
def initialize(name: nil, ratio: 1, size: nil, width: nil, height: nil)
|
7
8
|
@name = name
|
8
9
|
@ratio = ratio
|
9
10
|
@size = size
|
@@ -12,9 +13,49 @@ module RubyRich
|
|
12
13
|
@parent = nil
|
13
14
|
@x_offset = 0
|
14
15
|
@y_offset = 0
|
15
|
-
@width =
|
16
|
-
@height =
|
16
|
+
@width = width if width
|
17
|
+
@height = height if height
|
17
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
|
18
59
|
end
|
19
60
|
|
20
61
|
def root
|
@@ -33,6 +74,10 @@ module RubyRich
|
|
33
74
|
@children.concat(layouts)
|
34
75
|
end
|
35
76
|
|
77
|
+
def add_child(layout)
|
78
|
+
@children << layout
|
79
|
+
end
|
80
|
+
|
36
81
|
def update_content(content)
|
37
82
|
@content = content
|
38
83
|
end
|
@@ -56,18 +101,27 @@ module RubyRich
|
|
56
101
|
nil
|
57
102
|
end
|
58
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
|
+
|
59
113
|
def render
|
60
114
|
# 将缓冲区转换为字符串(每行用换行符连接)
|
61
|
-
|
115
|
+
buffer = render_to_buffer
|
116
|
+
buffer.map { |line| line.compact.join("") }.join("\n")
|
62
117
|
end
|
63
118
|
|
64
119
|
def render_to_buffer
|
65
120
|
# 初始化缓冲区(二维数组,每个元素代表一个字符)
|
66
121
|
buffer = Array.new(@height) { Array.new(@width, " ") }
|
67
|
-
|
68
122
|
# 递归填充内容到缓冲区
|
69
123
|
render_into(buffer)
|
70
|
-
|
124
|
+
render_dialog_into(buffer) if @dialog
|
71
125
|
return buffer
|
72
126
|
end
|
73
127
|
|
@@ -75,6 +129,21 @@ module RubyRich
|
|
75
129
|
puts render
|
76
130
|
end
|
77
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
|
+
|
78
147
|
def render_into(buffer)
|
79
148
|
children.each { |child| child.render_into(buffer) } if children
|
80
149
|
return unless content
|
data/lib/ruby_rich/live.rb
CHANGED
@@ -8,13 +8,13 @@ module RubyRich
|
|
8
8
|
def initialize
|
9
9
|
@cache = nil
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
def print_with_pos(x,y,char)
|
13
13
|
print "\e[?25l" # 隐藏光标
|
14
14
|
print "\e[#{y};#{x}H" # 移动光标到左上角
|
15
15
|
print char
|
16
16
|
end
|
17
|
-
|
17
|
+
|
18
18
|
def draw(buffer)
|
19
19
|
unless @cache
|
20
20
|
system("clear")
|
@@ -34,10 +34,12 @@ module RubyRich
|
|
34
34
|
end
|
35
35
|
|
36
36
|
class Live
|
37
|
+
attr_accessor :params, :app, :listening, :layout
|
37
38
|
class << self
|
38
|
-
def start(layout, refresh_rate: 30,
|
39
|
+
def start(layout, refresh_rate: 30, &proc)
|
39
40
|
setup_terminal
|
40
41
|
live = new(layout, refresh_rate)
|
42
|
+
proc.call(live) if proc
|
41
43
|
live.run(proc)
|
42
44
|
rescue => e
|
43
45
|
puts e.message
|
@@ -60,29 +62,44 @@ module RubyRich
|
|
60
62
|
|
61
63
|
def initialize(layout, refresh_rate)
|
62
64
|
@layout = layout
|
65
|
+
@layout.live = self
|
63
66
|
@refresh_rate = refresh_rate
|
64
67
|
@running = true
|
65
68
|
@last_frame = Time.now
|
66
69
|
@cursor = TTY::Cursor
|
67
70
|
@render = CacheRender.new
|
71
|
+
@console = RubyRich::Console.new
|
72
|
+
@params = {}
|
68
73
|
end
|
69
74
|
|
70
75
|
def run(proc = nil)
|
71
76
|
while @running
|
72
77
|
render_frame
|
73
|
-
|
78
|
+
if @listening
|
79
|
+
event_data = @console.get_key()
|
80
|
+
@layout.notify_listeners(event_data)
|
81
|
+
end
|
74
82
|
sleep 1.0 / @refresh_rate
|
75
83
|
end
|
76
84
|
end
|
77
85
|
|
78
86
|
def stop
|
79
87
|
@running = false
|
88
|
+
system("clear")
|
80
89
|
end
|
81
90
|
|
82
91
|
def move_cursor(x,y)
|
83
92
|
print @cursor.move_to(x, y)
|
84
93
|
end
|
85
94
|
|
95
|
+
def find_layout(name)
|
96
|
+
@layout[name]
|
97
|
+
end
|
98
|
+
|
99
|
+
def find_panel(name)
|
100
|
+
@layout[name].content
|
101
|
+
end
|
102
|
+
|
86
103
|
private
|
87
104
|
|
88
105
|
def render_frame
|
data/lib/ruby_rich/panel.rb
CHANGED
@@ -1,47 +1,72 @@
|
|
1
1
|
module RubyRich
|
2
2
|
class Panel
|
3
3
|
attr_accessor :width, :height, :content, :line_pos, :border_style, :title
|
4
|
-
|
5
|
-
|
6
|
-
|
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)
|
4
|
+
attr_accessor :title_align, :content_changed
|
5
|
+
|
6
|
+
def initialize(content = "", title: nil, border_style: :white, title_align: :center)
|
15
7
|
@content = content
|
16
8
|
@title = title
|
17
9
|
@border_style = border_style
|
18
10
|
@width = 0
|
19
11
|
@height = 0
|
20
12
|
@line_pos = 0
|
13
|
+
@title_align = title_align
|
21
14
|
end
|
22
15
|
|
23
16
|
def inner_width
|
24
17
|
@width - 2 # Account for border characters
|
25
18
|
end
|
26
19
|
|
27
|
-
def
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
31
49
|
end
|
32
|
-
width
|
33
50
|
end
|
34
51
|
|
35
52
|
def render
|
36
53
|
lines = []
|
37
|
-
color_code =
|
38
|
-
reset_code =
|
54
|
+
color_code = AnsiCode.color(@border_style) || AnsiCode.color(:white)
|
55
|
+
reset_code = AnsiCode.reset
|
39
56
|
|
40
57
|
# Top border
|
41
58
|
top_border = color_code + "┌"
|
42
59
|
if @title
|
43
|
-
title_text = "[ #{@title} ]"
|
44
|
-
|
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
|
45
70
|
else
|
46
71
|
top_border += '─' * (@width - 2)
|
47
72
|
end
|
@@ -50,13 +75,29 @@ module RubyRich
|
|
50
75
|
|
51
76
|
# Content area
|
52
77
|
content_lines = wrap_content(@content)
|
53
|
-
if
|
54
|
-
|
55
|
-
|
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
|
56
96
|
end
|
97
|
+
|
57
98
|
content_lines.each do |line|
|
58
99
|
lines << color_code + "│" + reset_code +
|
59
|
-
line + " "*(@width -
|
100
|
+
line + " "*(@width - line.display_width - 2) +
|
60
101
|
color_code + "│" + reset_code
|
61
102
|
end
|
62
103
|
|
@@ -74,8 +115,13 @@ module RubyRich
|
|
74
115
|
lines
|
75
116
|
end
|
76
117
|
|
77
|
-
def
|
118
|
+
def content=(new_content)
|
78
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
|
79
125
|
end
|
80
126
|
|
81
127
|
private
|
@@ -84,22 +130,32 @@ module RubyRich
|
|
84
130
|
result = []
|
85
131
|
current_line = ""
|
86
132
|
current_width = 0
|
87
|
-
|
88
|
-
text
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
93
149
|
else
|
94
150
|
result << current_line
|
95
|
-
current_line =
|
96
|
-
current_width =
|
151
|
+
current_line = token
|
152
|
+
current_width = token_width
|
97
153
|
end
|
98
154
|
end
|
99
|
-
|
100
|
-
#
|
155
|
+
|
156
|
+
# Add remaining line
|
101
157
|
result << current_line unless current_line.empty?
|
102
|
-
|
158
|
+
|
103
159
|
result
|
104
160
|
end
|
105
161
|
|
@@ -110,97 +166,18 @@ module RubyRich
|
|
110
166
|
end
|
111
167
|
end
|
112
168
|
end
|
113
|
-
module RubyRich
|
114
|
-
class RichPanel
|
115
|
-
# ANSI escape codes for styling
|
116
|
-
ANSI_CODES = {
|
117
|
-
reset: "\e[0m",
|
118
|
-
bold: "\e[1m",
|
119
|
-
underline: "\e[4m",
|
120
|
-
color: {
|
121
|
-
black: "\e[30m",
|
122
|
-
red: "\e[31m",
|
123
|
-
green: "\e[32m",
|
124
|
-
yellow: "\e[33m",
|
125
|
-
blue: "\e[34m",
|
126
|
-
magenta: "\e[35m",
|
127
|
-
cyan: "\e[36m",
|
128
|
-
white: "\e[37m"
|
129
|
-
},
|
130
|
-
background: {
|
131
|
-
black: "\e[40m",
|
132
|
-
red: "\e[41m",
|
133
|
-
green: "\e[42m",
|
134
|
-
yellow: "\e[43m",
|
135
|
-
blue: "\e[44m",
|
136
|
-
magenta: "\e[45m",
|
137
|
-
cyan: "\e[46m",
|
138
|
-
white: "\e[47m"
|
139
|
-
}
|
140
|
-
}
|
141
|
-
|
142
|
-
attr_accessor :title, :content, :border_color, :title_color, :footer
|
143
|
-
|
144
|
-
def initialize(content, title: nil, footer: nil, border_color: :white, title_color: :white)
|
145
|
-
@content = content.is_a?(String) ? content.split("\n") : content
|
146
|
-
@title = title
|
147
|
-
@footer = footer
|
148
|
-
@border_color = border_color
|
149
|
-
@title_color = title_color
|
150
|
-
end
|
151
|
-
|
152
|
-
def render
|
153
|
-
content_lines = format_content
|
154
|
-
panel_width = calculate_panel_width(content_lines)
|
155
|
-
|
156
|
-
lines = []
|
157
|
-
lines << top_border(panel_width)
|
158
|
-
lines += content_lines.map { |line| format_line(line, panel_width) }
|
159
|
-
lines << bottom_border(panel_width)
|
160
|
-
|
161
|
-
lines.join("\n")
|
162
|
-
end
|
163
|
-
|
164
|
-
private
|
165
|
-
|
166
|
-
def top_border(width)
|
167
|
-
title_text = @title ? colorize(" #{@title} ", @title_color) : ""
|
168
|
-
padding = (width - title_text.uncolorize.length - 2) / 2
|
169
|
-
"#{colorize("╭", @border_color)}#{colorize("─" * padding, @border_color)}#{title_text}#{colorize("─" * (width - title_text.uncolorize.length - padding - 2), @border_color)}#{colorize("╮", @border_color)}"
|
170
|
-
end
|
171
|
-
|
172
|
-
def bottom_border(width)
|
173
|
-
footer_text = @footer ? colorize(" #{@footer} ", @title_color) : ""
|
174
|
-
padding = (width - footer_text.uncolorize.length - 2) / 2
|
175
|
-
"#{colorize("╰", @border_color)}#{colorize("─" * padding, @border_color)}#{footer_text}#{colorize("─" * (width - footer_text.uncolorize.length - padding - 2), @border_color)}#{colorize("╯", @border_color)}"
|
176
|
-
end
|
177
|
-
|
178
|
-
def format_line(line, width)
|
179
|
-
"#{colorize("│", @border_color)} #{line.ljust(width - 4)} #{colorize("│", @border_color)}"
|
180
|
-
end
|
181
|
-
|
182
|
-
def format_content
|
183
|
-
@content.map(&:strip)
|
184
|
-
end
|
185
|
-
|
186
|
-
def calculate_panel_width(content_lines)
|
187
|
-
[
|
188
|
-
@title ? @title.uncolorize.length + 4 : 0,
|
189
|
-
@footer ? @footer.uncolorize.length + 4 : 0,
|
190
|
-
content_lines.map(&:length).max + 4
|
191
|
-
].max
|
192
|
-
end
|
193
|
-
|
194
|
-
def colorize(text, color)
|
195
|
-
code = ANSI_CODES[:color][color] || ""
|
196
|
-
"#{code}#{text}#{ANSI_CODES[:reset]}"
|
197
|
-
end
|
198
|
-
end
|
199
|
-
end
|
200
169
|
|
201
170
|
# Extend String to remove ANSI codes for alignment
|
202
171
|
class String
|
203
172
|
def uncolorize
|
204
173
|
gsub(/\e\[[0-9;]*m/, '')
|
205
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
|
206
183
|
end
|
data/lib/ruby_rich/print.rb
CHANGED
data/lib/ruby_rich/table.rb
CHANGED
data/lib/ruby_rich/text.rb
CHANGED
@@ -8,35 +8,6 @@ module RubyRich
|
|
8
8
|
warning: { color: :yellow, bold: true }
|
9
9
|
}
|
10
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
11
|
def self.set_theme(new_theme)
|
41
12
|
@@theme.merge!(new_theme)
|
42
13
|
end
|
@@ -47,18 +18,23 @@ module RubyRich
|
|
47
18
|
apply_theme(style) if style
|
48
19
|
end
|
49
20
|
|
50
|
-
def style(color:
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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)
|
57
33
|
self
|
58
34
|
end
|
59
35
|
|
60
36
|
def render
|
61
|
-
"#{@styles.join}#{@text}#{
|
37
|
+
"#{@styles.join}#{@text}#{AnsiCode.reset}"
|
62
38
|
end
|
63
39
|
|
64
40
|
private
|
data/lib/ruby_rich/version.rb
CHANGED
data/lib/ruby_rich.rb
CHANGED
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,6 +45,7 @@ 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
|
49
50
|
- lib/ruby_rich/dialog.rb
|
50
51
|
- lib/ruby_rich/layout.rb
|
@@ -73,7 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
73
74
|
- !ruby/object:Gem::Version
|
74
75
|
version: '0'
|
75
76
|
requirements: []
|
76
|
-
rubygems_version: 3.6.
|
77
|
+
rubygems_version: 3.6.4
|
77
78
|
specification_version: 4
|
78
79
|
summary: Rich text formatting and console output for Ruby
|
79
80
|
test_files: []
|