rich-ruby 1.0.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +546 -0
- data/examples/demo.rb +106 -0
- data/examples/showcase.rb +420 -0
- data/examples/smoke_test.rb +41 -0
- data/examples/stress_test.rb +604 -0
- data/examples/syntax_markdown_demo.rb +166 -0
- data/examples/verify.rb +215 -0
- data/examples/visual_demo.rb +145 -0
- data/lib/rich/_palettes.rb +148 -0
- data/lib/rich/box.rb +342 -0
- data/lib/rich/cells.rb +512 -0
- data/lib/rich/color.rb +628 -0
- data/lib/rich/color_triplet.rb +220 -0
- data/lib/rich/console.rb +549 -0
- data/lib/rich/control.rb +332 -0
- data/lib/rich/json.rb +254 -0
- data/lib/rich/layout.rb +314 -0
- data/lib/rich/markdown.rb +509 -0
- data/lib/rich/markup.rb +175 -0
- data/lib/rich/panel.rb +311 -0
- data/lib/rich/progress.rb +430 -0
- data/lib/rich/segment.rb +387 -0
- data/lib/rich/style.rb +433 -0
- data/lib/rich/syntax.rb +1145 -0
- data/lib/rich/table.rb +525 -0
- data/lib/rich/terminal_theme.rb +126 -0
- data/lib/rich/text.rb +433 -0
- data/lib/rich/tree.rb +220 -0
- data/lib/rich/version.rb +5 -0
- data/lib/rich/win32_console.rb +582 -0
- data/lib/rich.rb +108 -0
- metadata +106 -0
data/lib/rich/panel.rb
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "box"
|
|
4
|
+
require_relative "style"
|
|
5
|
+
require_relative "segment"
|
|
6
|
+
require_relative "cells"
|
|
7
|
+
require_relative "text"
|
|
8
|
+
|
|
9
|
+
module Rich
|
|
10
|
+
# A bordered panel container for content
|
|
11
|
+
class Panel
|
|
12
|
+
# @return [Object] Content to display
|
|
13
|
+
attr_reader :content
|
|
14
|
+
|
|
15
|
+
# @return [String, nil] Panel title
|
|
16
|
+
attr_reader :title
|
|
17
|
+
|
|
18
|
+
# @return [String, nil] Panel subtitle
|
|
19
|
+
attr_reader :subtitle
|
|
20
|
+
|
|
21
|
+
# @return [Box] Box style
|
|
22
|
+
attr_reader :box
|
|
23
|
+
|
|
24
|
+
# @return [Style, nil] Border style
|
|
25
|
+
attr_reader :border_style
|
|
26
|
+
|
|
27
|
+
# @return [Style, nil] Title style
|
|
28
|
+
attr_reader :title_style
|
|
29
|
+
|
|
30
|
+
# @return [Style, nil] Subtitle style
|
|
31
|
+
attr_reader :subtitle_style
|
|
32
|
+
|
|
33
|
+
# @return [Boolean] Expand to fill width
|
|
34
|
+
attr_reader :expand
|
|
35
|
+
|
|
36
|
+
# @return [Integer] Padding inside panel
|
|
37
|
+
attr_reader :padding
|
|
38
|
+
|
|
39
|
+
# @return [Integer, nil] Fixed width
|
|
40
|
+
attr_reader :width
|
|
41
|
+
|
|
42
|
+
# @return [Symbol] Title alignment
|
|
43
|
+
attr_reader :title_align
|
|
44
|
+
|
|
45
|
+
def initialize(
|
|
46
|
+
content,
|
|
47
|
+
title: nil,
|
|
48
|
+
subtitle: nil,
|
|
49
|
+
box: Box::ROUNDED,
|
|
50
|
+
border_style: nil,
|
|
51
|
+
title_style: nil,
|
|
52
|
+
subtitle_style: nil,
|
|
53
|
+
expand: true,
|
|
54
|
+
padding: 1,
|
|
55
|
+
width: nil,
|
|
56
|
+
title_align: :center
|
|
57
|
+
)
|
|
58
|
+
@content = content
|
|
59
|
+
@title = title
|
|
60
|
+
@subtitle = subtitle
|
|
61
|
+
@box = box
|
|
62
|
+
@border_style = border_style.is_a?(String) ? Style.parse(border_style) : border_style
|
|
63
|
+
@title_style = title_style.is_a?(String) ? Style.parse(title_style) : title_style
|
|
64
|
+
@subtitle_style = subtitle_style.is_a?(String) ? Style.parse(subtitle_style) : subtitle_style
|
|
65
|
+
@expand = expand
|
|
66
|
+
@padding = padding
|
|
67
|
+
@width = width
|
|
68
|
+
@title_align = title_align
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Render panel to segments
|
|
72
|
+
# @param max_width [Integer] Maximum width
|
|
73
|
+
# @return [Array<Segment>]
|
|
74
|
+
def to_segments(max_width: 80)
|
|
75
|
+
segments = []
|
|
76
|
+
inner_width = calculate_inner_width(max_width)
|
|
77
|
+
content_width = inner_width - @padding * 2
|
|
78
|
+
|
|
79
|
+
# Render content to lines
|
|
80
|
+
content_lines = render_content(content_width)
|
|
81
|
+
|
|
82
|
+
# Top border with title
|
|
83
|
+
segments.concat(render_top_border(inner_width))
|
|
84
|
+
segments << Segment.new("\n")
|
|
85
|
+
|
|
86
|
+
# Padding top
|
|
87
|
+
@padding.times do
|
|
88
|
+
segments.concat(render_empty_row(inner_width))
|
|
89
|
+
segments << Segment.new("\n")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Content rows
|
|
93
|
+
content_lines.each do |line|
|
|
94
|
+
segments.concat(render_content_row(line, inner_width))
|
|
95
|
+
segments << Segment.new("\n")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Padding bottom
|
|
99
|
+
@padding.times do
|
|
100
|
+
segments.concat(render_empty_row(inner_width))
|
|
101
|
+
segments << Segment.new("\n")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Bottom border with subtitle
|
|
105
|
+
segments.concat(render_bottom_border(inner_width))
|
|
106
|
+
|
|
107
|
+
segments
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Render panel to string with ANSI codes
|
|
111
|
+
# @param max_width [Integer] Maximum width
|
|
112
|
+
# @param color_system [Symbol] Color system
|
|
113
|
+
# @return [String]
|
|
114
|
+
def render(max_width: 80, color_system: ColorSystem::TRUECOLOR)
|
|
115
|
+
Segment.render(to_segments(max_width: max_width), color_system: color_system)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Print panel to console
|
|
119
|
+
# @param console [Console] Console to print to
|
|
120
|
+
def print_to(console)
|
|
121
|
+
rendered = render(max_width: console.width, color_system: console.color_system)
|
|
122
|
+
console.write(rendered)
|
|
123
|
+
console.write("\n")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
class << self
|
|
127
|
+
# Create a simple panel
|
|
128
|
+
# @param content [String] Content
|
|
129
|
+
# @param title [String, nil] Title
|
|
130
|
+
# @return [Panel]
|
|
131
|
+
def fit(content, title: nil)
|
|
132
|
+
new(content, title: title, expand: false)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def calculate_inner_width(max_width)
|
|
139
|
+
if @width
|
|
140
|
+
@width - 2 # Subtract borders
|
|
141
|
+
elsif @expand
|
|
142
|
+
max_width - 2
|
|
143
|
+
else
|
|
144
|
+
content_width = measure_content_width + @padding * 2
|
|
145
|
+
[content_width, max_width - 2].min
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def measure_content_width
|
|
150
|
+
case @content
|
|
151
|
+
when String
|
|
152
|
+
Cells.cell_len(@content)
|
|
153
|
+
when Text
|
|
154
|
+
@content.cell_length
|
|
155
|
+
else
|
|
156
|
+
Cells.cell_len(@content.to_s)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def render_content(width)
|
|
161
|
+
case @content
|
|
162
|
+
when String
|
|
163
|
+
wrap_text(@content, width)
|
|
164
|
+
when Text
|
|
165
|
+
@content.wrap(width).map { |t| t.to_segments }
|
|
166
|
+
else
|
|
167
|
+
wrap_text(@content.to_s, width)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def wrap_text(text, width)
|
|
172
|
+
lines = []
|
|
173
|
+
text.split("\n").each do |line|
|
|
174
|
+
if Cells.cell_len(line) <= width
|
|
175
|
+
lines << [Segment.new(line)]
|
|
176
|
+
else
|
|
177
|
+
# Simple wrapping
|
|
178
|
+
current_line = +""
|
|
179
|
+
current_width = 0
|
|
180
|
+
|
|
181
|
+
line.each_char do |char|
|
|
182
|
+
char_width = Cells.char_width(char)
|
|
183
|
+
if current_width + char_width > width
|
|
184
|
+
lines << [Segment.new(current_line)]
|
|
185
|
+
current_line = +char
|
|
186
|
+
current_width = char_width
|
|
187
|
+
else
|
|
188
|
+
current_line << char
|
|
189
|
+
current_width += char_width
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
lines << [Segment.new(current_line)] unless current_line.empty?
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
lines
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def render_top_border(inner_width)
|
|
200
|
+
segments = []
|
|
201
|
+
|
|
202
|
+
if @title
|
|
203
|
+
title_text = " #{@title} "
|
|
204
|
+
title_width = Cells.cell_len(title_text)
|
|
205
|
+
|
|
206
|
+
if title_width < inner_width
|
|
207
|
+
remaining = inner_width - title_width
|
|
208
|
+
case @title_align
|
|
209
|
+
when :left
|
|
210
|
+
left = 2
|
|
211
|
+
right = remaining - 2
|
|
212
|
+
when :right
|
|
213
|
+
left = remaining - 2
|
|
214
|
+
right = 2
|
|
215
|
+
else # :center
|
|
216
|
+
left = remaining / 2
|
|
217
|
+
right = remaining - left
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
segments << Segment.new(@box.top_left, style: @border_style)
|
|
221
|
+
segments << Segment.new(@box.horizontal * left, style: @border_style)
|
|
222
|
+
segments << Segment.new(title_text, style: @title_style || @border_style)
|
|
223
|
+
segments << Segment.new(@box.horizontal * right, style: @border_style)
|
|
224
|
+
segments << Segment.new(@box.top_right, style: @border_style)
|
|
225
|
+
else
|
|
226
|
+
segments << Segment.new(@box.top_edge(inner_width), style: @border_style)
|
|
227
|
+
end
|
|
228
|
+
else
|
|
229
|
+
segments << Segment.new(@box.top_left, style: @border_style)
|
|
230
|
+
segments << Segment.new(@box.horizontal * inner_width, style: @border_style)
|
|
231
|
+
segments << Segment.new(@box.top_right, style: @border_style)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
segments
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def render_bottom_border(inner_width)
|
|
238
|
+
segments = []
|
|
239
|
+
|
|
240
|
+
if @subtitle
|
|
241
|
+
sub_text = " #{@subtitle} "
|
|
242
|
+
sub_width = Cells.cell_len(sub_text)
|
|
243
|
+
|
|
244
|
+
if sub_width < inner_width
|
|
245
|
+
remaining = inner_width - sub_width
|
|
246
|
+
case @title_align
|
|
247
|
+
when :left
|
|
248
|
+
left = 2
|
|
249
|
+
right = remaining - 2
|
|
250
|
+
when :right
|
|
251
|
+
left = remaining - 2
|
|
252
|
+
right = 2
|
|
253
|
+
else # :center
|
|
254
|
+
left = remaining / 2
|
|
255
|
+
right = remaining - left
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
segments << Segment.new(@box.bottom_left, style: @border_style)
|
|
259
|
+
segments << Segment.new(@box.horizontal * left, style: @border_style)
|
|
260
|
+
segments << Segment.new(sub_text, style: @subtitle_style || @border_style)
|
|
261
|
+
segments << Segment.new(@box.horizontal * right, style: @border_style)
|
|
262
|
+
segments << Segment.new(@box.bottom_right, style: @border_style)
|
|
263
|
+
else
|
|
264
|
+
segments << Segment.new(@box.bottom_edge(inner_width), style: @border_style)
|
|
265
|
+
end
|
|
266
|
+
else
|
|
267
|
+
segments << Segment.new(@box.bottom_left, style: @border_style)
|
|
268
|
+
segments << Segment.new(@box.horizontal * inner_width, style: @border_style)
|
|
269
|
+
segments << Segment.new(@box.bottom_right, style: @border_style)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
segments
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def render_content_row(content_segments, inner_width)
|
|
276
|
+
segments = []
|
|
277
|
+
|
|
278
|
+
# Left border
|
|
279
|
+
segments << Segment.new(@box.vertical, style: @border_style)
|
|
280
|
+
|
|
281
|
+
# Left padding
|
|
282
|
+
segments << Segment.new(" " * @padding)
|
|
283
|
+
|
|
284
|
+
# Content
|
|
285
|
+
content_width = content_segments.sum(&:cell_length)
|
|
286
|
+
segments.concat(content_segments)
|
|
287
|
+
|
|
288
|
+
# Right padding (fill to width)
|
|
289
|
+
remaining = inner_width - @padding * 2 - content_width
|
|
290
|
+
segments << Segment.new(" " * [remaining, 0].max)
|
|
291
|
+
|
|
292
|
+
# Right padding
|
|
293
|
+
segments << Segment.new(" " * @padding)
|
|
294
|
+
|
|
295
|
+
# Right border
|
|
296
|
+
segments << Segment.new(@box.vertical, style: @border_style)
|
|
297
|
+
|
|
298
|
+
segments
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def render_empty_row(inner_width)
|
|
302
|
+
segments = []
|
|
303
|
+
|
|
304
|
+
segments << Segment.new(@box.vertical, style: @border_style)
|
|
305
|
+
segments << Segment.new(" " * inner_width)
|
|
306
|
+
segments << Segment.new(@box.vertical, style: @border_style)
|
|
307
|
+
|
|
308
|
+
segments
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|