ruby_rich 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: af37dc09091efdc06215f083a176d91ce1b9ecf240ba22f4471b739b9eaeb890
4
+ data.tar.gz: 57e7a9e0378b20353b94d9f815d9c96e1865f96128257857921c80e5f7a31fb9
5
+ SHA512:
6
+ metadata.gz: 440203e48f904f11270482a9e80f9edb28787e64302d6d6dcf8527eaee2ec57028303e8d8a331b6bfe2ce45c445aba122ceaa6cdd975a524ce62f8c55fe2638f
7
+ data.tar.gz: 4f043b626479294f8d785e6e181614e1b2b0ca867f5714a8e6323ccb6b96433ad0ec7ef432b173ecc4cfbe9e6ecc215799174fffcb5953a3d0ce41bad8964f74
@@ -0,0 +1,53 @@
1
+ module RubyRich
2
+ class Console
3
+ def initialize
4
+ @lines = []
5
+ @buffer = []
6
+ @layout = { spacing: 1, align: :left }
7
+ end
8
+
9
+ def set_layout(spacing: 1, align: :left)
10
+ @layout[:spacing] = spacing
11
+ @layout[:align] = align
12
+ end
13
+
14
+ def add_line(text)
15
+ @lines << text
16
+ end
17
+
18
+ def clear
19
+ print "\e[H\e[2J"
20
+ end
21
+
22
+ def render
23
+ clear
24
+ @lines.each_with_index do |line, index|
25
+ formatted_line = format_line(line)
26
+ @buffer << formatted_line
27
+ puts formatted_line
28
+ puts "\n" * @layout[:spacing] if index < @lines.size - 1
29
+ end
30
+ end
31
+
32
+ def update_line(index, text)
33
+ return unless index.between?(0, @lines.size - 1)
34
+
35
+ @lines[index] = text
36
+ render
37
+ end
38
+
39
+ private
40
+
41
+ def format_line(line)
42
+ content = line.is_a?(RichText) ? line.render : line
43
+ case @layout[:align]
44
+ when :center
45
+ content.center(80)
46
+ when :right
47
+ content.rjust(80)
48
+ else
49
+ content
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,26 @@
1
+ module RubyRich
2
+ class ProgressBar
3
+ def initialize(total, width: 50, style: :default)
4
+ @total = total
5
+ @progress = 0
6
+ @width = width
7
+ @style = style
8
+ end
9
+
10
+ def advance(amount)
11
+ @progress += amount
12
+ @progress = @total if @progress > @total
13
+ render
14
+ end
15
+
16
+ def render
17
+ percentage = (@progress.to_f / @total * 100).to_i
18
+ completed_width = (@progress.to_f / @total * @width).to_i
19
+ incomplete_width = @width - completed_width
20
+
21
+ bar = "[#{"=" * completed_width}#{" " * incomplete_width}]"
22
+ print "\r#{bar} #{percentage}%"
23
+ puts if @progress == @total
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,94 @@
1
+ module RubyRich
2
+ class RichPanel
3
+ # ANSI escape codes for styling
4
+ ANSI_CODES = {
5
+ reset: "\e[0m",
6
+ bold: "\e[1m",
7
+ underline: "\e[4m",
8
+ color: {
9
+ black: "\e[30m",
10
+ red: "\e[31m",
11
+ green: "\e[32m",
12
+ yellow: "\e[33m",
13
+ blue: "\e[34m",
14
+ magenta: "\e[35m",
15
+ cyan: "\e[36m",
16
+ white: "\e[37m"
17
+ },
18
+ background: {
19
+ black: "\e[40m",
20
+ red: "\e[41m",
21
+ green: "\e[42m",
22
+ yellow: "\e[43m",
23
+ blue: "\e[44m",
24
+ magenta: "\e[45m",
25
+ cyan: "\e[46m",
26
+ white: "\e[47m"
27
+ }
28
+ }
29
+
30
+ attr_accessor :title, :content, :border_color, :title_color, :footer
31
+
32
+ def initialize(content, title: nil, footer: nil, border_color: :white, title_color: :white)
33
+ @content = content.is_a?(String) ? content.split("\n") : content
34
+ @title = title
35
+ @footer = footer
36
+ @border_color = border_color
37
+ @title_color = title_color
38
+ end
39
+
40
+ def render
41
+ content_lines = format_content
42
+ panel_width = calculate_panel_width(content_lines)
43
+
44
+ lines = []
45
+ lines << top_border(panel_width)
46
+ lines += content_lines.map { |line| format_line(line, panel_width) }
47
+ lines << bottom_border(panel_width)
48
+
49
+ lines.join("\n")
50
+ end
51
+
52
+ private
53
+
54
+ def top_border(width)
55
+ title_text = @title ? colorize(" #{@title} ", @title_color) : ""
56
+ padding = (width - title_text.uncolorize.length - 2) / 2
57
+ "#{colorize("╭", @border_color)}#{colorize("─" * padding, @border_color)}#{title_text}#{colorize("─" * (width - title_text.uncolorize.length - padding - 2), @border_color)}#{colorize("╮", @border_color)}"
58
+ end
59
+
60
+ def bottom_border(width)
61
+ footer_text = @footer ? colorize(" #{@footer} ", @title_color) : ""
62
+ padding = (width - footer_text.uncolorize.length - 2) / 2
63
+ "#{colorize("╰", @border_color)}#{colorize("─" * padding, @border_color)}#{footer_text}#{colorize("─" * (width - footer_text.uncolorize.length - padding - 2), @border_color)}#{colorize("╯", @border_color)}"
64
+ end
65
+
66
+ def format_line(line, width)
67
+ "#{colorize("│", @border_color)} #{line.ljust(width - 4)} #{colorize("│", @border_color)}"
68
+ end
69
+
70
+ def format_content
71
+ @content.map(&:strip)
72
+ end
73
+
74
+ def calculate_panel_width(content_lines)
75
+ [
76
+ @title ? @title.uncolorize.length + 4 : 0,
77
+ @footer ? @footer.uncolorize.length + 4 : 0,
78
+ content_lines.map(&:length).max + 4
79
+ ].max
80
+ end
81
+
82
+ def colorize(text, color)
83
+ code = ANSI_CODES[:color][color] || ""
84
+ "#{code}#{text}#{ANSI_CODES[:reset]}"
85
+ end
86
+ end
87
+ end
88
+
89
+ # Extend String to remove ANSI codes for alignment
90
+ class String
91
+ def uncolorize
92
+ gsub(/\e\[[0-9;]*m/, '')
93
+ end
94
+ end
@@ -0,0 +1,60 @@
1
+ module RubyRich
2
+ class RichPrint
3
+ def initialize
4
+ @style_regex = /\[([\w\s]+)\](.*?)\[\/[\w]*\]/
5
+ end
6
+
7
+ def print(*args)
8
+ processed_args = args.map do |arg|
9
+ next arg unless arg.is_a?(String)
10
+
11
+ # 处理表情符号
12
+ text = if arg.start_with?(':') && arg.end_with?(':')
13
+ Emoji.find_by_alias(arg[1..-2])&.raw || arg
14
+ else
15
+ arg
16
+ end
17
+
18
+ # 处理样式标记
19
+ while text.match?(@style_regex)
20
+ text = text.gsub(@style_regex) do |_|
21
+ style, content = $1, $2
22
+ apply_style(content, style)
23
+ end
24
+ end
25
+
26
+ text
27
+ end
28
+
29
+ puts processed_args.join(' ')
30
+ end
31
+
32
+ private
33
+
34
+ def apply_style(content, style)
35
+ style_methods = style.downcase.split
36
+ rich_text = RichRuby::RichText.new(content)
37
+ style.downcase.split.each do |method|
38
+ case method
39
+ when 'bold'
40
+ rich_text.style(bold: true)
41
+ when 'italic'
42
+ rich_text.style(italic: true)
43
+ when 'underline'
44
+ rich_text.style(underline: true)
45
+ when 'blink'
46
+ rich_text.style(blink: true)
47
+ when 'red', 'blue', 'green', 'yellow', 'magenta', 'cyan', 'white', 'black'
48
+ rich_text.style(color: method.to_sym)
49
+ end
50
+ end
51
+ rich_text.render
52
+ end
53
+ end
54
+
55
+ # 创建全局打印方法
56
+ $rich_print = RichPrint.new
57
+ def print(*args)
58
+ $rich_print.print(*args)
59
+ end
60
+ end
@@ -0,0 +1,80 @@
1
+ module RubyRich
2
+ class RichText
3
+ # 默认主题
4
+ @@theme = {
5
+ error: { color: :red, bold: true },
6
+ success: { color: :green, bold: true },
7
+ info: { color: :cyan },
8
+ warning: { color: :yellow, bold: true }
9
+ }
10
+
11
+ # ANSI 转义码常量
12
+ ANSI_CODES = {
13
+ reset: "\e[0m",
14
+ bold: "\e[1m",
15
+ italic: "\e[3m",
16
+ underline: "\e[4m",
17
+ blink: "\e[5m",
18
+ color: {
19
+ black: "\e[30m",
20
+ red: "\e[31m",
21
+ green: "\e[32m",
22
+ yellow: "\e[33m",
23
+ blue: "\e[34m",
24
+ magenta: "\e[35m",
25
+ cyan: "\e[36m",
26
+ white: "\e[37m"
27
+ },
28
+ background: {
29
+ black: "\e[40m",
30
+ red: "\e[41m",
31
+ green: "\e[42m",
32
+ yellow: "\e[43m",
33
+ blue: "\e[44m",
34
+ magenta: "\e[45m",
35
+ cyan: "\e[46m",
36
+ white: "\e[47m"
37
+ }
38
+ }
39
+
40
+ def self.set_theme(new_theme)
41
+ @@theme.merge!(new_theme)
42
+ end
43
+
44
+ def initialize(text, style: nil)
45
+ @text = text
46
+ @styles = []
47
+ apply_theme(style) if style
48
+ end
49
+
50
+ def style(color: nil, background: nil, bold: false, italic: false, underline: false, blink: false)
51
+ @styles << ANSI_CODES[:color][color] if color
52
+ @styles << ANSI_CODES[:background][background] if background
53
+ @styles << ANSI_CODES[:bold] if bold
54
+ @styles << ANSI_CODES[:italic] if italic
55
+ @styles << ANSI_CODES[:underline] if underline
56
+ @styles << ANSI_CODES[:blink] if blink
57
+ self
58
+ end
59
+
60
+ def render
61
+ "#{@styles.join}#{@text}#{ANSI_CODES[:reset]}"
62
+ end
63
+
64
+ private
65
+
66
+ def add_style(code, error_message)
67
+ if code
68
+ @styles << code
69
+ else
70
+ raise ArgumentError, error_message
71
+ end
72
+ end
73
+
74
+ def apply_theme(style)
75
+ theme_styles = @@theme[style]
76
+ raise ArgumentError, "Undefined theme style: #{style}" unless theme_styles
77
+ style(**theme_styles)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,137 @@
1
+ require 'unicode/display_width'
2
+
3
+ module RubyRich
4
+ class Table
5
+ attr_accessor :headers, :rows, :align, :row_height
6
+
7
+ def initialize(headers: [], align: :left, row_height: 1)
8
+ @headers = headers.map { |h| format_cell(h) }
9
+ @rows = []
10
+ @align = align
11
+ @row_height = row_height
12
+ end
13
+
14
+ def add_row(row)
15
+ @rows << row.map { |cell| format_cell(cell) }
16
+ end
17
+
18
+ def render
19
+ column_widths = calculate_column_widths
20
+ lines = []
21
+
22
+ # Render headers
23
+ lines << render_row(@headers, column_widths, bold: true)
24
+ lines << "-" * (column_widths.sum { |w| w + 3 } + 1)
25
+
26
+ # Render rows
27
+ @rows.each do |row|
28
+ lines.concat(render_multiline_row(row, column_widths))
29
+ end
30
+
31
+ lines.join("\n")
32
+ end
33
+
34
+ private
35
+
36
+ def format_cell(cell)
37
+ cell.is_a?(RichRuby::RichText) ? cell : RichRuby::RichText.new(cell.to_s)
38
+ end
39
+
40
+ def calculate_column_widths
41
+ widths = Array.new(@headers.size, 0)
42
+
43
+ # Calculate widths from headers
44
+ @headers.each_with_index do |header, i|
45
+ header_text = header.respond_to?(:render) ? header.render : header.to_s
46
+ header_width = Unicode::DisplayWidth.of(header_text.gsub(/\e\[[0-9;]*m/, ''))
47
+ widths[i] = [widths[i], header_width].max
48
+ end
49
+
50
+ # Calculate widths from rows
51
+ @rows.each do |row|
52
+ row.each_with_index do |cell, i|
53
+ cell_lines = cell.render.split("\n")
54
+ cell_lines.each do |line|
55
+ # Remove ANSI escape sequences before calculating width
56
+ plain_line = line.gsub(/\e\[[0-9;]*m/, '')
57
+ width = Unicode::DisplayWidth.of(plain_line)
58
+ widths[i] = [widths[i], width].max
59
+ end
60
+ end
61
+ end
62
+
63
+ widths
64
+ end
65
+
66
+ def render_row(row, column_widths, bold: false)
67
+ row.map.with_index do |cell, i|
68
+ content = bold ? cell.render : align_cell(cell.render, column_widths[i])
69
+ align_cell(content, column_widths[i])
70
+ end.join(" | ").prepend("| ").concat(" |")
71
+ end
72
+
73
+ def render_multiline_row(row, column_widths)
74
+ # Prepare each cell's lines
75
+ row_lines = row.map.with_index do |cell, i|
76
+ # 获取单元格的样式序列
77
+ style_sequence = cell.render.match(/\e\[[0-9;]*m/)&.to_s || ""
78
+ reset_sequence = style_sequence.empty? ? "" : "\e[0m"
79
+
80
+ # 分割成多行并保持样式
81
+ cell_content = cell.render.split("\n")
82
+
83
+ # 为每一行添加样式
84
+ cell_content.map! { |line|
85
+ line = line.gsub(/\e\[[0-9;]*m/, '') # 移除可能存在的样式序列
86
+ style_sequence + line + reset_sequence
87
+ }
88
+
89
+ # 填充到指定的行高
90
+ padded_content = cell_content + [" "] * [@row_height - cell_content.size, 0].max
91
+
92
+ # 对每一行应用对齐,保持样式
93
+ padded_content.map { |line| align_cell(line, column_widths[i]) }
94
+ end
95
+
96
+ # Normalize row height
97
+ max_height = row_lines.map(&:size).max
98
+ row_lines.each do |lines|
99
+ width = column_widths[row_lines.index(lines)]
100
+ style_sequence = lines.first.match(/\e\[[0-9;]*m/)&.to_s || ""
101
+ reset_sequence = style_sequence.empty? ? "" : "\e[0m"
102
+ lines.fill(style_sequence + " " * width + reset_sequence, lines.size...max_height)
103
+ end
104
+
105
+ # Render each line of the row
106
+ (0...max_height).map do |line_index|
107
+ row_lines.map { |lines| lines[line_index] }.join(" | ").prepend("| ").concat(" |")
108
+ end
109
+ end
110
+
111
+ def align_cell(content, width)
112
+ style_sequences = content.scan(/\e\[[0-9;]*m/)
113
+ plain_content = content.gsub(/\e\[[0-9;]*m/, '')
114
+
115
+ # 计算实际显示宽度
116
+ display_width = Unicode::DisplayWidth.of(plain_content)
117
+ padding_needed = width - display_width
118
+
119
+ padded_content = case @align
120
+ when :center
121
+ left_padding = padding_needed / 2
122
+ right_padding = padding_needed - left_padding
123
+ " " * left_padding + plain_content + " " * right_padding
124
+ when :right
125
+ " " * padding_needed + plain_content
126
+ else
127
+ plain_content + " " * padding_needed
128
+ end
129
+
130
+ if style_sequences.any?
131
+ style_sequences.first + padded_content + "\e[0m"
132
+ else
133
+ padded_content
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,3 @@
1
+ module RubyRich
2
+ VERSION = "0.1.0"
3
+ end
data/lib/ruby_rich.rb ADDED
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # 加载所有依赖的 gem
4
+ require 'rouge'
5
+ require 'tty-cursor'
6
+ require 'tty-screen'
7
+ require 'redcarpet'
8
+
9
+ # 加载所有内部模块
10
+ require_relative 'ruby_rich/console'
11
+ require_relative 'ruby_rich/table'
12
+ require_relative 'ruby_rich/progress_bar'
13
+ require_relative 'ruby_rich/rich_text'
14
+ require_relative 'ruby_rich/rich_print'
15
+ require_relative 'ruby_rich/rich_panel'
16
+ require_relative 'ruby_rich/version'
17
+
18
+ # 定义主模块
19
+ module RubyRich
20
+ class Error < StandardError; end
21
+
22
+ # 提供一个便捷方法来创建控制台实例
23
+ def self.console
24
+ @console ||= Console.new
25
+ end
26
+
27
+ # 提供一个便捷方法来创建富文本
28
+ def self.text(content = '')
29
+ RichText.new(content)
30
+ end
31
+
32
+ # 提供一个便捷方法来创建表格
33
+ def self.table
34
+ Table.new
35
+ end
36
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby_rich
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - zhuang biaowei
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2025-02-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rake
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '13.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '13.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: minitest
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '5.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '5.0'
40
+ description: A Ruby gem providing rich text formatting, progress bars, tables and
41
+ other console output enhancements
42
+ email: zbw@kaiyuanshe.org
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - lib/ruby_rich.rb
48
+ - lib/ruby_rich/console.rb
49
+ - 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
+ - lib/ruby_rich/table.rb
54
+ - lib/ruby_rich/version.rb
55
+ homepage: https://github.com/zhuangbiaowei/ruby_rich
56
+ licenses:
57
+ - MIT
58
+ metadata: {}
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: 2.7.0
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubygems_version: 3.6.2
74
+ specification_version: 4
75
+ summary: Rich text formatting and console output for Ruby
76
+ test_files: []