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.
@@ -0,0 +1,314 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "segment"
4
+ require_relative "cells"
5
+ require_relative "style"
6
+
7
+ module Rich
8
+ # Display content side by side in columns
9
+ class Columns
10
+ # @return [Array] Items to display
11
+ attr_reader :items
12
+
13
+ # @return [Integer, nil] Column count
14
+ attr_reader :column_count
15
+
16
+ # @return [Integer] Padding between columns
17
+ attr_reader :padding
18
+
19
+ # @return [Boolean] Equal width columns
20
+ attr_reader :equal
21
+
22
+ # @return [Boolean] Expand to fill width
23
+ attr_reader :expand
24
+
25
+ def initialize(
26
+ items = [],
27
+ column_count: nil,
28
+ padding: 1,
29
+ equal: false,
30
+ expand: true
31
+ )
32
+ @items = items.to_a
33
+ @column_count = column_count
34
+ @padding = padding
35
+ @equal = equal
36
+ @expand = expand
37
+ end
38
+
39
+ # Add an item
40
+ # @param item [Object] Item to add
41
+ # @return [self]
42
+ def add(item)
43
+ @items << item
44
+ self
45
+ end
46
+
47
+ # Render to segments
48
+ # @param max_width [Integer] Maximum width
49
+ # @return [Array<Segment>]
50
+ def to_segments(max_width: 80)
51
+ return [] if @items.empty?
52
+
53
+ segments = []
54
+
55
+ # Calculate column count if not specified
56
+ num_columns = @column_count || calculate_column_count(max_width)
57
+ num_columns = [num_columns, @items.length].min
58
+
59
+ # Calculate column widths
60
+ col_widths = calculate_widths(max_width, num_columns)
61
+
62
+ # Render items in rows
63
+ @items.each_slice(num_columns) do |row_items|
64
+ row_items.each_with_index do |item, col_index|
65
+ content = item.to_s
66
+ width = col_widths[col_index]
67
+
68
+ # Render content
69
+ content_width = Cells.cell_len(content)
70
+ if content_width > width
71
+ content = truncate(content, width)
72
+ content_width = Cells.cell_len(content)
73
+ end
74
+
75
+ segments << Segment.new(content)
76
+ segments << Segment.new(" " * (width - content_width))
77
+
78
+ # Padding between columns (not after last)
79
+ if col_index < row_items.length - 1
80
+ segments << Segment.new(" " * @padding)
81
+ end
82
+ end
83
+
84
+ segments << Segment.new("\n")
85
+ end
86
+
87
+ segments
88
+ end
89
+
90
+ # Render to string
91
+ # @param max_width [Integer] Maximum width
92
+ # @param color_system [Symbol] Color system
93
+ # @return [String]
94
+ def render(max_width: 80, color_system: ColorSystem::TRUECOLOR)
95
+ Segment.render(to_segments(max_width: max_width), color_system: color_system)
96
+ end
97
+
98
+ private
99
+
100
+ def calculate_column_count(max_width)
101
+ return 1 if @items.empty?
102
+
103
+ # Calculate based on average item width
104
+ avg_width = @items.map { |i| Cells.cell_len(i.to_s) }.sum / @items.length
105
+ min_col_width = [avg_width, 10].max
106
+
107
+ ((max_width + @padding) / (min_col_width + @padding)).clamp(1, 10)
108
+ end
109
+
110
+ def calculate_widths(max_width, num_columns)
111
+ available = max_width - (@padding * (num_columns - 1))
112
+
113
+ if @equal
114
+ width = available / num_columns
115
+ Array.new(num_columns, width)
116
+ else
117
+ # Calculate based on content
118
+ widths = Array.new(num_columns, 0)
119
+
120
+ @items.each_with_index do |item, index|
121
+ col = index % num_columns
122
+ item_width = Cells.cell_len(item.to_s)
123
+ widths[col] = [widths[col], item_width].max
124
+ end
125
+
126
+ # Scale if total exceeds available
127
+ total = widths.sum
128
+ if total > available
129
+ ratio = available.to_f / total
130
+ widths.map! { |w| [( w * ratio).to_i, 5].max }
131
+ elsif @expand
132
+ extra = (available - total) / num_columns
133
+ widths.map! { |w| w + extra }
134
+ end
135
+
136
+ widths
137
+ end
138
+ end
139
+
140
+ def truncate(text, max_width)
141
+ result = +""
142
+ current = 0
143
+
144
+ text.each_char do |char|
145
+ char_width = Cells.char_width(char)
146
+ break if current + char_width > max_width
147
+
148
+ result << char
149
+ current += char_width
150
+ end
151
+
152
+ result
153
+ end
154
+ end
155
+
156
+ # Live updating display
157
+ class Live
158
+ # @return [Console] Console for output
159
+ attr_reader :console
160
+
161
+ # @return [Float] Refresh rate
162
+ attr_reader :refresh_rate
163
+
164
+ # @return [Boolean] Transient (clear on exit)
165
+ attr_reader :transient
166
+
167
+ # Default refresh rate (Windows is slower)
168
+ DEFAULT_REFRESH = Gem.win_platform? ? 0.2 : 0.1
169
+
170
+ def initialize(
171
+ console: nil,
172
+ refresh_rate: DEFAULT_REFRESH,
173
+ transient: false
174
+ )
175
+ @console = console || Console.new
176
+ @refresh_rate = refresh_rate
177
+ @transient = transient
178
+ @renderable = nil
179
+ @started = false
180
+ @lines_rendered = 0
181
+ @last_render = nil
182
+ end
183
+
184
+ # Update the renderable content
185
+ # @param renderable [Object] Content to display
186
+ def update(renderable)
187
+ @renderable = renderable
188
+ refresh
189
+ end
190
+
191
+ # Start live display
192
+ # @yield Block to execute with live updates
193
+ def start
194
+ @started = true
195
+ @console.hide_cursor
196
+
197
+ if block_given?
198
+ begin
199
+ yield self
200
+ ensure
201
+ stop
202
+ end
203
+ end
204
+ end
205
+
206
+ # Stop live display
207
+ def stop
208
+ return unless @started
209
+
210
+ if @transient && @lines_rendered > 0
211
+ # Clear rendered content
212
+ @console.write("\e[#{@lines_rendered}A\e[J")
213
+ end
214
+
215
+ @console.show_cursor
216
+ @started = false
217
+ end
218
+
219
+ # Refresh display if needed
220
+ def refresh
221
+ return unless @started
222
+ return unless @renderable
223
+
224
+ now = Time.now
225
+ return if @last_render && now - @last_render < @refresh_rate
226
+
227
+ render_update
228
+ @last_render = now
229
+ end
230
+
231
+ private
232
+
233
+ def render_update
234
+ # Clear previous output
235
+ if @lines_rendered > 0
236
+ @console.write("\e[#{@lines_rendered}A\e[J")
237
+ end
238
+
239
+ # Render new content
240
+ output = render_content(@renderable)
241
+ @console.write(output)
242
+
243
+ # Count lines
244
+ @lines_rendered = output.count("\n")
245
+ end
246
+
247
+ def render_content(obj)
248
+ case obj
249
+ when Panel, Table, Tree
250
+ obj.render(max_width: @console.width, color_system: @console.color_system)
251
+ else
252
+ "#{obj}\n"
253
+ end
254
+ end
255
+ end
256
+
257
+ # Status display with spinner
258
+ class Status
259
+ # @return [Spinner] Spinner animation
260
+ attr_reader :spinner
261
+
262
+ # @return [String] Status message
263
+ attr_accessor :message
264
+
265
+ # @return [Console] Console
266
+ attr_reader :console
267
+
268
+ def initialize(message = "", console: nil, spinner: nil)
269
+ @message = message
270
+ @console = console || Console.new
271
+ @spinner = spinner || Spinner.new
272
+ @started = false
273
+ end
274
+
275
+ # Start status display
276
+ # @yield Block to execute
277
+ def start
278
+ @started = true
279
+ @console.hide_cursor
280
+
281
+ if block_given?
282
+ begin
283
+ yield self
284
+ ensure
285
+ stop
286
+ end
287
+ end
288
+ end
289
+
290
+ # Stop status display
291
+ def stop
292
+ return unless @started
293
+
294
+ @console.write("\r\e[K")
295
+ @console.show_cursor
296
+ @started = false
297
+ end
298
+
299
+ # Update message
300
+ # @param new_message [String] New message
301
+ def update(new_message)
302
+ @message = new_message
303
+ refresh
304
+ end
305
+
306
+ # Refresh display
307
+ def refresh
308
+ return unless @started
309
+
310
+ @spinner.update
311
+ @console.write("\r\e[K#{@spinner.frame} #{@message}")
312
+ end
313
+ end
314
+ end