ruby_rich 0.2.0 → 0.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 22f7fb9cb3aa52b99809ca646ca234aa1f098967a8cfd13caddb49d71b3e455a
4
- data.tar.gz: adbd0feae9e0a6bb2f4b631b390a1a067b6426ab99fb94ce19c4f0b3923c55e3
3
+ metadata.gz: 5e70688e9f049e91cc514d529ece0fe5dfad219b45117a8da518d5252f93e0f9
4
+ data.tar.gz: 17d1c30049d5f295a51815e6b6c1e2e1900e6e6567dbb561ba433a7ccf57e4ee
5
5
  SHA512:
6
- metadata.gz: abd1b8ecbcc18f755cd72182f59774fb2a28dcf763610c2486ba3f80f00671ee590a6c132692d1ad981d7d01fa66964204ac904ee710ac801ca03c7ccf3c9486
7
- data.tar.gz: 3cd9279be0f8a2f47a451263ca6813ac1a6ba46490dc4db829f52336307e17edd0a8c3eefdd816ee79d07cb61d91b2ba6088cfe7f0eec59673f0f3dbebffbe96
6
+ metadata.gz: 7ebc794ba61ee56bd49e18b424cc2c9a821dd0b1581f26a4a1fa96147362ca8696446359d06d6cffc9f99f7e2dcc21fcc6ceef692235e799c827b3c8252ed805
7
+ data.tar.gz: 752acc4ba0e823af2e40ad2e338f341fa46a7f46d8769798d1112fca4b175e7f564d9e7813e67876b43ce1724a62a208043b59ba1932b1475dffbda44208dedc
@@ -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
@@ -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,17 @@ 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
+ # 检查是否有后续输入(粘贴内容会有多个字符)
82
+ has_more = IO.select([io], nil, nil, 0)
83
+ return has_more ? {:name => :string, :value => char} : {:name=>:enter}
83
84
  end
84
85
  # 单独处理 Tab 键(ASCII 9)
85
86
  if char == "\t"
86
- return :tab
87
+ return {:name=>:tab}
87
88
  elsif char.ord == 0x07F
88
- return :backspace
89
+ return {:name=>:backspace}
89
90
  elsif char == "\e" # 检测到转义序列
90
91
  sequence = ''
91
92
  begin
@@ -96,13 +97,17 @@ module RubyRich
96
97
  retry if IO.select([io], nil, nil, 0.01)
97
98
  rescue EOFError
98
99
  end
99
- return ESCAPE_SEQUENCES[sequence] || :escape
100
+ if sequence.empty?
101
+ return {:name => :escape}
102
+ else
103
+ return {:name => ESCAPE_SEQUENCES[sequence]} || {:name => :escape}
104
+ end
100
105
  # 处理 Ctrl 组合键(排除 Tab 和回车)
101
106
  elsif char.ord.between?(1, 8) || char.ord.between?(10, 26)
102
107
  ctrl_char = (char.ord + 64).chr.downcase
103
- return :"ctrl_#{ctrl_char}"
108
+ return {:name =>"ctrl_#{ctrl_char}".to_sym}
104
109
  else
105
- char
110
+ {:name => :string, :value => char}
106
111
  end
107
112
  end
108
113
  end
@@ -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 initialize(title: "", content: "", buttons: ["OK"])
6
- @title = title
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
- def show
15
- Console.raw do
16
- loop do
17
- Console.clear
18
- render
19
- handle_input(Console.get_key)
20
- break if @result
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
- @result
34
+ str.strip
24
35
  end
25
36
 
26
- private
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 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
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
- @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
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 show
132
- super
133
- @result == "确定" ? @input : nil
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
@@ -1,9 +1,10 @@
1
1
  module RubyRich
2
2
  class Layout
3
- attr_accessor :name, :ratio, :size, :children, :content, :parent, :x_offset, :y_offset, :width, :height
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 = nil
16
- @height = nil
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
- render_to_buffer.map { |line| line.compact.join("") }.join("\n")
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
@@ -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, full_screen: false, &proc)
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
- proc.call(self) if proc
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
@@ -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
- 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)
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 calculate_string_width(str)
28
- width = 0
29
- str.each_char do |char|
30
- width += Unicode::DisplayWidth.of(char)
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 = COLORS[@border_style] || COLORS[:white]
38
- reset_code = COLORS[:reset]
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
- top_border += title_text + '─' * (@width - calculate_string_width(@title)-6)
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 content_lines.size > @height - 2
54
- @line_pos = content_lines.size - @height + 2
55
- content_lines=content_lines[@line_pos..-1]
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 - calculate_string_width(line) - 2) +
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 update_content(new_content)
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,37 @@ module RubyRich
84
130
  result = []
85
131
  current_line = ""
86
132
  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
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
+ start_color = nil
138
+ tokens.each do |token|
139
+ # Calculate width for regular text, ANSI codes have 0 width
140
+ if token.start_with?("\e[")
141
+ if token == "\e[0m"
142
+ start_color = nil
143
+ else
144
+ start_color = token
145
+ end
146
+ token_width = 0
147
+ else
148
+ token_width = token.chars.sum { |c| Unicode::DisplayWidth.of(c) }
149
+ end
150
+
151
+ if current_width + token_width <= @width - 4
152
+ current_line += token
153
+ current_width += token_width
93
154
  else
94
155
  result << current_line
95
- current_line = char
96
- current_width = char_width
156
+ current_line = start_color.to_s+token
157
+ current_width = token_width
97
158
  end
98
159
  end
99
-
100
- # 添加最后一行
160
+
161
+ # Add remaining line
101
162
  result << current_line unless current_line.empty?
102
-
163
+
103
164
  result
104
165
  end
105
166
 
@@ -110,97 +171,18 @@ module RubyRich
110
171
  end
111
172
  end
112
173
  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
174
 
201
175
  # Extend String to remove ANSI codes for alignment
202
176
  class String
203
177
  def uncolorize
204
178
  gsub(/\e\[[0-9;]*m/, '')
205
179
  end
180
+
181
+ def display_width
182
+ width = 0
183
+ self.uncolorize.each_char do |char|
184
+ width += Unicode::DisplayWidth.of(char)
185
+ end
186
+ width
187
+ end
206
188
  end
@@ -33,7 +33,7 @@ module RubyRich
33
33
 
34
34
  def apply_style(content, style)
35
35
  style_methods = style.downcase.split
36
- rich_text = RichRuby::RichText.new(content)
36
+ rich_text = RubyRich::RichText.new(content)
37
37
  style.downcase.split.each do |method|
38
38
  case method
39
39
  when 'bold'
@@ -34,7 +34,7 @@ module RubyRich
34
34
  private
35
35
 
36
36
  def format_cell(cell)
37
- cell.is_a?(RichRuby::RichText) ? cell : RichRuby::RichText.new(cell.to_s)
37
+ cell.is_a?(RubyRich::RichText) ? cell : RubyRich::RichText.new(cell.to_s)
38
38
  end
39
39
 
40
40
  def calculate_column_widths
@@ -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: 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
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}#{ANSI_CODES[:reset]}"
37
+ "#{@styles.join}#{@text}#{AnsiCode.reset}"
62
38
  end
63
39
 
64
40
  private
@@ -1,3 +1,3 @@
1
1
  module RubyRich
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.1"
3
3
  end
data/lib/ruby_rich.rb CHANGED
@@ -16,6 +16,7 @@ require_relative 'ruby_rich/text'
16
16
  require_relative 'ruby_rich/print'
17
17
  require_relative 'ruby_rich/panel'
18
18
  require_relative 'ruby_rich/dialog'
19
+ require_relative 'ruby_rich/ansi_code'
19
20
  require_relative 'ruby_rich/version'
20
21
 
21
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.2.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - zhuang biaowei
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-02-10 00:00:00.000000000 Z
10
+ date: 2025-03-14 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.2
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: []