ratatui_ruby 0.9.1 → 0.10.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/.builds/ruby-3.2.yml +1 -1
- data/.builds/ruby-3.3.yml +1 -1
- data/.builds/ruby-3.4.yml +1 -1
- data/.builds/ruby-4.0.0.yml +1 -1
- data/AGENTS.md +2 -1
- data/CHANGELOG.md +98 -0
- data/REUSE.toml +5 -0
- data/Rakefile +1 -1
- data/Steepfile +49 -0
- data/doc/concepts/debugging.md +401 -0
- data/doc/getting_started/quickstart.md +8 -3
- data/doc/images/app_all_events.png +0 -0
- data/doc/images/app_color_picker.png +0 -0
- data/doc/images/app_debugging_showcase.gif +0 -0
- data/doc/images/app_debugging_showcase.png +0 -0
- data/doc/images/app_login_form.png +0 -0
- data/doc/images/app_stateful_interaction.png +0 -0
- data/doc/images/verify_quickstart_dsl.png +0 -0
- data/doc/images/verify_quickstart_layout.png +0 -0
- data/doc/images/verify_quickstart_lifecycle.png +0 -0
- data/doc/images/verify_readme_usage.png +0 -0
- data/doc/images/widget_barchart.png +0 -0
- data/doc/images/widget_block.png +0 -0
- data/doc/images/widget_box.png +0 -0
- data/doc/images/widget_calendar.png +0 -0
- data/doc/images/widget_canvas.png +0 -0
- data/doc/images/widget_cell.png +0 -0
- data/doc/images/widget_center.png +0 -0
- data/doc/images/widget_chart.png +0 -0
- data/doc/images/widget_gauge.png +0 -0
- data/doc/images/widget_layout_split.png +0 -0
- data/doc/images/widget_line_gauge.png +0 -0
- data/doc/images/widget_list.png +0 -0
- data/doc/images/widget_map.png +0 -0
- data/doc/images/widget_overlay.png +0 -0
- data/doc/images/widget_popup.png +0 -0
- data/doc/images/widget_ratatui_logo.png +0 -0
- data/doc/images/widget_ratatui_mascot.png +0 -0
- data/doc/images/widget_rect.png +0 -0
- data/doc/images/widget_render.png +0 -0
- data/doc/images/widget_rich_text.png +0 -0
- data/doc/images/widget_scroll_text.png +0 -0
- data/doc/images/widget_scrollbar.png +0 -0
- data/doc/images/widget_sparkline.png +0 -0
- data/doc/images/widget_style_colors.png +0 -0
- data/doc/images/widget_table.png +0 -0
- data/doc/images/widget_tabs.png +0 -0
- data/doc/images/widget_text_width.png +0 -0
- data/doc/troubleshooting/async.md +4 -0
- data/examples/app_debugging_showcase/README.md +119 -0
- data/examples/app_debugging_showcase/app.rb +318 -0
- data/examples/widget_canvas/app.rb +19 -14
- data/examples/widget_gauge/app.rb +18 -3
- data/examples/widget_layout_split/app.rb +10 -4
- data/examples/widget_list/app.rb +22 -6
- data/examples/widget_rect/app.rb +7 -6
- data/examples/widget_rich_text/app.rb +62 -37
- data/examples/widget_style_colors/app.rb +26 -47
- data/examples/widget_table/app.rb +28 -5
- data/examples/widget_text_width/app.rb +6 -4
- data/ext/ratatui_ruby/Cargo.lock +48 -1
- data/ext/ratatui_ruby/Cargo.toml +6 -2
- data/ext/ratatui_ruby/src/color.rs +82 -0
- data/ext/ratatui_ruby/src/errors.rs +28 -0
- data/ext/ratatui_ruby/src/events.rs +15 -14
- data/ext/ratatui_ruby/src/lib.rs +56 -0
- data/ext/ratatui_ruby/src/rendering.rs +3 -1
- data/ext/ratatui_ruby/src/style.rs +48 -21
- data/ext/ratatui_ruby/src/terminal.rs +40 -9
- data/ext/ratatui_ruby/src/text.rs +21 -9
- data/ext/ratatui_ruby/src/widgets/chart.rs +2 -1
- data/ext/ratatui_ruby/src/widgets/layout.rs +90 -2
- data/ext/ratatui_ruby/src/widgets/list.rs +6 -5
- data/ext/ratatui_ruby/src/widgets/overlay.rs +2 -1
- data/ext/ratatui_ruby/src/widgets/table.rs +7 -6
- data/ext/ratatui_ruby/src/widgets/table_state.rs +55 -0
- data/ext/ratatui_ruby/src/widgets/tabs.rs +3 -2
- data/lib/ratatui_ruby/buffer/cell.rb +25 -15
- data/lib/ratatui_ruby/buffer.rb +134 -2
- data/lib/ratatui_ruby/cell.rb +13 -5
- data/lib/ratatui_ruby/debug.rb +215 -0
- data/lib/ratatui_ruby/event/key.rb +3 -2
- data/lib/ratatui_ruby/event.rb +1 -1
- data/lib/ratatui_ruby/layout/constraint.rb +49 -0
- data/lib/ratatui_ruby/layout/layout.rb +119 -13
- data/lib/ratatui_ruby/layout/position.rb +55 -0
- data/lib/ratatui_ruby/layout/rect.rb +188 -0
- data/lib/ratatui_ruby/layout/size.rb +55 -0
- data/lib/ratatui_ruby/layout.rb +4 -0
- data/lib/ratatui_ruby/style/color.rb +149 -0
- data/lib/ratatui_ruby/style/style.rb +51 -4
- data/lib/ratatui_ruby/style.rb +2 -0
- data/lib/ratatui_ruby/symbols.rb +435 -0
- data/lib/ratatui_ruby/synthetic_events.rb +1 -1
- data/lib/ratatui_ruby/table_state.rb +51 -0
- data/lib/ratatui_ruby/terminal_lifecycle.rb +2 -1
- data/lib/ratatui_ruby/test_helper/event_injection.rb +6 -1
- data/lib/ratatui_ruby/test_helper.rb +9 -0
- data/lib/ratatui_ruby/text/line.rb +245 -0
- data/lib/ratatui_ruby/text/span.rb +158 -0
- data/lib/ratatui_ruby/text.rb +99 -0
- data/lib/ratatui_ruby/tui/canvas_factories.rb +103 -0
- data/lib/ratatui_ruby/tui/core.rb +13 -2
- data/lib/ratatui_ruby/tui/layout_factories.rb +50 -3
- data/lib/ratatui_ruby/tui/state_factories.rb +42 -0
- data/lib/ratatui_ruby/tui/text_factories.rb +40 -0
- data/lib/ratatui_ruby/tui/widget_factories.rb +135 -60
- data/lib/ratatui_ruby/tui.rb +22 -1
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +2 -0
- data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +2 -0
- data/lib/ratatui_ruby/widgets/bar_chart.rb +30 -20
- data/lib/ratatui_ruby/widgets/block.rb +14 -6
- data/lib/ratatui_ruby/widgets/calendar.rb +2 -0
- data/lib/ratatui_ruby/widgets/canvas.rb +56 -0
- data/lib/ratatui_ruby/widgets/cell.rb +2 -0
- data/lib/ratatui_ruby/widgets/center.rb +2 -0
- data/lib/ratatui_ruby/widgets/chart.rb +6 -0
- data/lib/ratatui_ruby/widgets/clear.rb +2 -0
- data/lib/ratatui_ruby/widgets/coerceable_widget.rb +77 -0
- data/lib/ratatui_ruby/widgets/cursor.rb +2 -0
- data/lib/ratatui_ruby/widgets/gauge.rb +61 -3
- data/lib/ratatui_ruby/widgets/line_gauge.rb +66 -4
- data/lib/ratatui_ruby/widgets/list.rb +87 -3
- data/lib/ratatui_ruby/widgets/list_item.rb +2 -0
- data/lib/ratatui_ruby/widgets/overlay.rb +2 -0
- data/lib/ratatui_ruby/widgets/paragraph.rb +4 -0
- data/lib/ratatui_ruby/widgets/ratatui_logo.rb +2 -0
- data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +2 -0
- data/lib/ratatui_ruby/widgets/row.rb +45 -0
- data/lib/ratatui_ruby/widgets/scrollbar.rb +2 -0
- data/lib/ratatui_ruby/widgets/shape/label.rb +2 -0
- data/lib/ratatui_ruby/widgets/sparkline.rb +21 -13
- data/lib/ratatui_ruby/widgets/table.rb +13 -3
- data/lib/ratatui_ruby/widgets/tabs.rb +6 -4
- data/lib/ratatui_ruby/widgets.rb +1 -0
- data/lib/ratatui_ruby.rb +40 -9
- data/sig/examples/app_all_events/model/app_model.rbs +23 -0
- data/sig/examples/app_all_events/model/event_entry.rbs +15 -8
- data/sig/examples/app_all_events/model/timestamp.rbs +1 -1
- data/sig/examples/app_all_events/view.rbs +1 -1
- data/sig/examples/app_stateful_interaction/app.rbs +5 -5
- data/sig/examples/widget_block_demo/app.rbs +6 -6
- data/sig/manifest.yaml +5 -0
- data/sig/patches/data.rbs +26 -0
- data/sig/patches/debugger__.rbs +8 -0
- data/sig/ratatui_ruby/buffer/cell.rbs +46 -0
- data/sig/ratatui_ruby/buffer.rbs +18 -0
- data/sig/ratatui_ruby/cell.rbs +44 -0
- data/sig/ratatui_ruby/clear.rbs +18 -0
- data/sig/ratatui_ruby/constraint.rbs +26 -0
- data/sig/ratatui_ruby/debug.rbs +45 -0
- data/sig/ratatui_ruby/draw.rbs +30 -0
- data/sig/ratatui_ruby/event.rbs +68 -8
- data/sig/ratatui_ruby/frame.rbs +4 -4
- data/sig/ratatui_ruby/interfaces.rbs +25 -0
- data/sig/ratatui_ruby/layout/constraint.rbs +39 -0
- data/sig/ratatui_ruby/layout/layout.rbs +45 -0
- data/sig/ratatui_ruby/layout/position.rbs +18 -0
- data/sig/ratatui_ruby/layout/rect.rbs +64 -0
- data/sig/ratatui_ruby/layout/size.rbs +18 -0
- data/sig/ratatui_ruby/output_guard.rbs +23 -0
- data/sig/ratatui_ruby/ratatui_ruby.rbs +83 -4
- data/sig/ratatui_ruby/rect.rbs +17 -0
- data/sig/ratatui_ruby/style/color.rbs +22 -0
- data/sig/ratatui_ruby/style/style.rbs +29 -0
- data/sig/ratatui_ruby/symbols.rbs +141 -0
- data/sig/ratatui_ruby/synthetic_events.rbs +21 -0
- data/sig/ratatui_ruby/table_state.rbs +6 -0
- data/sig/ratatui_ruby/terminal_lifecycle.rbs +31 -0
- data/sig/ratatui_ruby/test_helper/event_injection.rbs +2 -2
- data/sig/ratatui_ruby/test_helper/snapshot.rbs +22 -3
- data/sig/ratatui_ruby/test_helper/style_assertions.rbs +8 -1
- data/sig/ratatui_ruby/test_helper/test_doubles.rbs +7 -3
- data/sig/ratatui_ruby/text/line.rbs +27 -0
- data/sig/ratatui_ruby/text/span.rbs +23 -0
- data/sig/ratatui_ruby/text.rbs +12 -0
- data/sig/ratatui_ruby/tui/buffer_factories.rbs +1 -1
- data/sig/ratatui_ruby/tui/canvas_factories.rbs +23 -5
- data/sig/ratatui_ruby/tui/core.rbs +2 -2
- data/sig/ratatui_ruby/tui/layout_factories.rbs +16 -2
- data/sig/ratatui_ruby/tui/state_factories.rbs +8 -3
- data/sig/ratatui_ruby/tui/style_factories.rbs +3 -1
- data/sig/ratatui_ruby/tui/text_factories.rbs +7 -4
- data/sig/ratatui_ruby/tui/widget_factories.rbs +123 -30
- data/sig/ratatui_ruby/widgets/bar_chart.rbs +95 -0
- data/sig/ratatui_ruby/widgets/block.rbs +51 -0
- data/sig/ratatui_ruby/widgets/calendar.rbs +45 -0
- data/sig/ratatui_ruby/widgets/canvas.rbs +95 -0
- data/sig/ratatui_ruby/widgets/chart.rbs +91 -0
- data/sig/ratatui_ruby/widgets/coerceable_widget.rbs +26 -0
- data/sig/ratatui_ruby/widgets/gauge.rbs +44 -0
- data/sig/ratatui_ruby/widgets/line_gauge.rbs +48 -0
- data/sig/ratatui_ruby/widgets/list.rbs +63 -0
- data/sig/ratatui_ruby/widgets/misc.rbs +158 -0
- data/sig/ratatui_ruby/widgets/paragraph.rbs +45 -0
- data/sig/ratatui_ruby/widgets/row.rbs +43 -0
- data/sig/ratatui_ruby/widgets/scrollbar.rbs +53 -0
- data/sig/ratatui_ruby/widgets/shape/label.rbs +37 -0
- data/sig/ratatui_ruby/widgets/sparkline.rbs +45 -0
- data/sig/ratatui_ruby/widgets/table.rbs +78 -0
- data/sig/ratatui_ruby/widgets/tabs.rbs +44 -0
- data/sig/ratatui_ruby/{schema/list_item.rbs → widgets.rbs} +4 -4
- data/tasks/steep.rake +11 -0
- metadata +80 -63
- data/doc/contributors/v1.0.0_blockers.md +0 -870
- data/doc/troubleshooting/debugging.md +0 -101
- data/lib/ratatui_ruby/schema/bar_chart/bar.rb +0 -47
- data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +0 -25
- data/lib/ratatui_ruby/schema/bar_chart.rb +0 -287
- data/lib/ratatui_ruby/schema/block.rb +0 -198
- data/lib/ratatui_ruby/schema/calendar.rb +0 -84
- data/lib/ratatui_ruby/schema/canvas.rb +0 -239
- data/lib/ratatui_ruby/schema/center.rb +0 -67
- data/lib/ratatui_ruby/schema/chart.rb +0 -159
- data/lib/ratatui_ruby/schema/clear.rb +0 -62
- data/lib/ratatui_ruby/schema/constraint.rb +0 -151
- data/lib/ratatui_ruby/schema/cursor.rb +0 -50
- data/lib/ratatui_ruby/schema/gauge.rb +0 -72
- data/lib/ratatui_ruby/schema/layout.rb +0 -122
- data/lib/ratatui_ruby/schema/line_gauge.rb +0 -80
- data/lib/ratatui_ruby/schema/list.rb +0 -135
- data/lib/ratatui_ruby/schema/list_item.rb +0 -51
- data/lib/ratatui_ruby/schema/overlay.rb +0 -51
- data/lib/ratatui_ruby/schema/paragraph.rb +0 -107
- data/lib/ratatui_ruby/schema/ratatui_logo.rb +0 -31
- data/lib/ratatui_ruby/schema/ratatui_mascot.rb +0 -36
- data/lib/ratatui_ruby/schema/rect.rb +0 -174
- data/lib/ratatui_ruby/schema/row.rb +0 -76
- data/lib/ratatui_ruby/schema/scrollbar.rb +0 -143
- data/lib/ratatui_ruby/schema/shape/label.rb +0 -76
- data/lib/ratatui_ruby/schema/sparkline.rb +0 -142
- data/lib/ratatui_ruby/schema/style.rb +0 -97
- data/lib/ratatui_ruby/schema/table.rb +0 -141
- data/lib/ratatui_ruby/schema/tabs.rb +0 -85
- data/lib/ratatui_ruby/schema/text.rb +0 -217
- data/sig/examples/app_all_events/model/events.rbs +0 -15
- data/sig/examples/app_all_events/view_state.rbs +0 -21
- data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +0 -22
- data/sig/ratatui_ruby/schema/bar_chart/bar_group.rbs +0 -19
- data/sig/ratatui_ruby/schema/bar_chart.rbs +0 -38
- data/sig/ratatui_ruby/schema/block.rbs +0 -18
- data/sig/ratatui_ruby/schema/calendar.rbs +0 -23
- data/sig/ratatui_ruby/schema/canvas.rbs +0 -81
- data/sig/ratatui_ruby/schema/center.rbs +0 -17
- data/sig/ratatui_ruby/schema/chart.rbs +0 -39
- data/sig/ratatui_ruby/schema/constraint.rbs +0 -30
- data/sig/ratatui_ruby/schema/cursor.rbs +0 -16
- data/sig/ratatui_ruby/schema/draw.rbs +0 -33
- data/sig/ratatui_ruby/schema/gauge.rbs +0 -23
- data/sig/ratatui_ruby/schema/layout.rbs +0 -27
- data/sig/ratatui_ruby/schema/line_gauge.rbs +0 -24
- data/sig/ratatui_ruby/schema/list.rbs +0 -28
- data/sig/ratatui_ruby/schema/overlay.rbs +0 -15
- data/sig/ratatui_ruby/schema/paragraph.rbs +0 -20
- data/sig/ratatui_ruby/schema/ratatui_logo.rbs +0 -14
- data/sig/ratatui_ruby/schema/ratatui_mascot.rbs +0 -17
- data/sig/ratatui_ruby/schema/rect.rbs +0 -48
- data/sig/ratatui_ruby/schema/row.rbs +0 -28
- data/sig/ratatui_ruby/schema/scrollbar.rbs +0 -42
- data/sig/ratatui_ruby/schema/sparkline.rbs +0 -22
- data/sig/ratatui_ruby/schema/style.rbs +0 -19
- data/sig/ratatui_ruby/schema/table.rbs +0 -32
- data/sig/ratatui_ruby/schema/tabs.rbs +0 -21
- data/sig/ratatui_ruby/schema/text.rbs +0 -31
- /data/lib/ratatui_ruby/{schema/draw.rb → draw.rb} +0 -0
|
@@ -190,6 +190,8 @@ module RatatuiRuby
|
|
|
190
190
|
# SPDX-SnippetEnd
|
|
191
191
|
#++
|
|
192
192
|
class Canvas < Data.define(:shapes, :x_bounds, :y_bounds, :marker, :block, :background_color)
|
|
193
|
+
include CoerceableWidget
|
|
194
|
+
|
|
193
195
|
##
|
|
194
196
|
# :attr_reader: shapes
|
|
195
197
|
# Array of shapes to render.
|
|
@@ -236,6 +238,60 @@ module RatatuiRuby
|
|
|
236
238
|
background_color:
|
|
237
239
|
)
|
|
238
240
|
end
|
|
241
|
+
|
|
242
|
+
# Converts canvas coordinates to normalized grid coordinates.
|
|
243
|
+
#
|
|
244
|
+
# Hit testing and layout decisions need to know where a canvas point
|
|
245
|
+
# falls within the drawing surface. This method maps from the canvas
|
|
246
|
+
# coordinate system to normalized [0.0, 1.0] coordinates.
|
|
247
|
+
#
|
|
248
|
+
# Use it to determine if a click or touch event lands within the
|
|
249
|
+
# canvas bounds, and where proportionally.
|
|
250
|
+
#
|
|
251
|
+
# [x] X coordinate in canvas coordinate system.
|
|
252
|
+
# [y] Y coordinate in canvas coordinate system.
|
|
253
|
+
#
|
|
254
|
+
# Returns an Array <tt>[normalized_x, normalized_y]</tt> where each
|
|
255
|
+
# value is between 0.0 and 1.0, or <tt>nil</tt> if the point is
|
|
256
|
+
# outside the canvas bounds.
|
|
257
|
+
#
|
|
258
|
+
# === Example
|
|
259
|
+
#
|
|
260
|
+
#--
|
|
261
|
+
# SPDX-SnippetBegin
|
|
262
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
263
|
+
# SPDX-License-Identifier: MIT-0
|
|
264
|
+
#++
|
|
265
|
+
# canvas = Canvas.new(x_bounds: [0.0, 100.0], y_bounds: [0.0, 50.0])
|
|
266
|
+
# canvas.get_point(50.0, 25.0) # => [0.5, 0.5] (center)
|
|
267
|
+
# canvas.get_point(0.0, 0.0) # => [0.0, 1.0] (bottom-left)
|
|
268
|
+
# canvas.get_point(101.0, 0.0) # => nil (out of bounds)
|
|
269
|
+
#--
|
|
270
|
+
# SPDX-SnippetEnd
|
|
271
|
+
#++
|
|
272
|
+
def get_point(x, y)
|
|
273
|
+
left, right = x_bounds
|
|
274
|
+
bottom, top = y_bounds
|
|
275
|
+
|
|
276
|
+
# Check bounds
|
|
277
|
+
return nil if x < left || x > right || y < bottom || y > top
|
|
278
|
+
|
|
279
|
+
width = right - left
|
|
280
|
+
height = top - bottom
|
|
281
|
+
|
|
282
|
+
# Avoid division by zero
|
|
283
|
+
return nil if width <= 0.0 || height <= 0.0
|
|
284
|
+
|
|
285
|
+
# Normalize to [0.0, 1.0] range
|
|
286
|
+
normalized_x = (x - left) / width
|
|
287
|
+
normalized_y = (top - y) / height # Y inverted: top is 0, bottom is 1
|
|
288
|
+
|
|
289
|
+
[normalized_x, normalized_y]
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Ruby-idiomatic aliases (TIMTOWTDI)
|
|
293
|
+
alias point get_point
|
|
294
|
+
alias [] get_point
|
|
239
295
|
end
|
|
240
296
|
end
|
|
241
297
|
end
|
|
@@ -14,6 +14,8 @@ module RatatuiRuby
|
|
|
14
14
|
# [style] Style
|
|
15
15
|
# [labels_alignment] Symbol (<tt>:left</tt>, <tt>:center</tt>, <tt>:right</tt>)
|
|
16
16
|
class Axis < Data.define(:title, :bounds, :labels, :style, :labels_alignment)
|
|
17
|
+
include CoerceableWidget
|
|
18
|
+
|
|
17
19
|
##
|
|
18
20
|
# :attr_reader: title
|
|
19
21
|
# Label for the axis (String).
|
|
@@ -59,6 +61,8 @@ module RatatuiRuby
|
|
|
59
61
|
# [marker] Symbol (<tt>:dot</tt>, <tt>:braille</tt>, <tt>:block</tt>, <tt>:bar</tt>, <tt>:half_block</tt>)
|
|
60
62
|
# [graph_type] Symbol (<tt>:line</tt>, <tt>:scatter</tt>)
|
|
61
63
|
class Dataset < Data.define(:name, :data, :style, :marker, :graph_type)
|
|
64
|
+
include CoerceableWidget
|
|
65
|
+
|
|
62
66
|
##
|
|
63
67
|
# :attr_reader: name
|
|
64
68
|
# Name for logical identification or legend.
|
|
@@ -118,6 +122,8 @@ module RatatuiRuby
|
|
|
118
122
|
#
|
|
119
123
|
# ruby examples/widget_chart/app.rb
|
|
120
124
|
class Chart < Data.define(:datasets, :x_axis, :y_axis, :block, :style, :legend_position, :hidden_legend_constraints)
|
|
125
|
+
include CoerceableWidget
|
|
126
|
+
|
|
121
127
|
##
|
|
122
128
|
# :attr_reader: datasets
|
|
123
129
|
# Array of Dataset objects to plot.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
module RatatuiRuby
|
|
9
|
+
module Widgets
|
|
10
|
+
# Mixin that provides DWIM hash coercion for widget classes.
|
|
11
|
+
#
|
|
12
|
+
# When users call `tui.table(hash)` instead of `tui.table(**hash)`,
|
|
13
|
+
# Ruby's `...` forwarding passes the Hash as a positional argument,
|
|
14
|
+
# causing cryptic TypeErrors at the Rust FFI boundary.
|
|
15
|
+
#
|
|
16
|
+
# This mixin provides a `coerce_args` class method that detects
|
|
17
|
+
# this pattern and automatically splats the hash into keyword arguments.
|
|
18
|
+
#
|
|
19
|
+
# === Behavior
|
|
20
|
+
#
|
|
21
|
+
# - **Production mode**: Unknown keys are silently ignored
|
|
22
|
+
# - **Debug mode (RR_DEBUG=1)**: Raises ArgumentError to catch typos early
|
|
23
|
+
#
|
|
24
|
+
# === Usage
|
|
25
|
+
#
|
|
26
|
+
#--
|
|
27
|
+
# SPDX-SnippetBegin
|
|
28
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
29
|
+
# SPDX-License-Identifier: MIT-0
|
|
30
|
+
#++
|
|
31
|
+
# class Table < Data.define(:rows, :widths, ...)
|
|
32
|
+
# include CoerceableWidget
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# # In WidgetFactories:
|
|
36
|
+
# def table(first = nil, **kwargs)
|
|
37
|
+
# Widgets::Table.coerce_args(first, kwargs)
|
|
38
|
+
# end
|
|
39
|
+
#--
|
|
40
|
+
# SPDX-SnippetEnd
|
|
41
|
+
#++
|
|
42
|
+
module CoerceableWidget
|
|
43
|
+
##
|
|
44
|
+
# Hook called when this module is included in a widget class.
|
|
45
|
+
#
|
|
46
|
+
# Extends the class with ClassMethods and defines KNOWN_KEYS constant
|
|
47
|
+
# from the Data.define members for validation.
|
|
48
|
+
#
|
|
49
|
+
# [base] The class including this module.
|
|
50
|
+
def self.included(base)
|
|
51
|
+
base.extend(ClassMethods)
|
|
52
|
+
base.const_set(:KNOWN_KEYS, base.members.freeze) unless base.const_defined?(:KNOWN_KEYS)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Class methods extended onto widget classes.
|
|
56
|
+
module ClassMethods
|
|
57
|
+
# Coerces a bare Hash argument into keyword arguments.
|
|
58
|
+
#
|
|
59
|
+
# @param first [Hash, nil] First positional argument (bare hash case)
|
|
60
|
+
# @param kwargs [Hash] Keyword arguments (normal splatted case)
|
|
61
|
+
# @return [Object] New instance of the widget class
|
|
62
|
+
# @raise [ArgumentError] In debug mode, if unknown keys are present
|
|
63
|
+
def coerce_args(first, kwargs)
|
|
64
|
+
if first.is_a?(Hash) && kwargs.empty?
|
|
65
|
+
unknown = first.keys - self::KNOWN_KEYS
|
|
66
|
+
if unknown.any? && RatatuiRuby::Debug.enabled?
|
|
67
|
+
raise ArgumentError, "#{name}: unknown keys #{unknown.inspect}"
|
|
68
|
+
end
|
|
69
|
+
new(**first.slice(*self::KNOWN_KEYS))
|
|
70
|
+
else
|
|
71
|
+
new(**kwargs)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -32,6 +32,8 @@ module RatatuiRuby
|
|
|
32
32
|
# - {Component-based implementation using Frame API}[link:/examples/app_color_picker/app_rb.html]
|
|
33
33
|
# - RatatuiRuby::Frame#set_cursor_position (Frame API alternative)
|
|
34
34
|
class Cursor < Data.define(:x, :y)
|
|
35
|
+
include CoerceableWidget
|
|
36
|
+
|
|
35
37
|
##
|
|
36
38
|
# :attr_reader: x
|
|
37
39
|
# X coordinate (column).
|
|
@@ -23,6 +23,8 @@ module RatatuiRuby
|
|
|
23
23
|
#
|
|
24
24
|
# ruby examples/widget_gauge/app.rb
|
|
25
25
|
class Gauge < Data.define(:ratio, :label, :style, :gauge_style, :block, :use_unicode)
|
|
26
|
+
include CoerceableWidget
|
|
27
|
+
|
|
26
28
|
##
|
|
27
29
|
# :attr_reader: ratio
|
|
28
30
|
# Progress ratio from 0.0 to 1.0.
|
|
@@ -62,9 +64,16 @@ module RatatuiRuby
|
|
|
62
64
|
# [gauge_style] Style object for the filled bar (optional).
|
|
63
65
|
# [block] Block widget (optional).
|
|
64
66
|
# [use_unicode] Boolean (default: true).
|
|
67
|
+
#
|
|
68
|
+
# Raises ArgumentError if percent is not 0..100.
|
|
65
69
|
def initialize(ratio: nil, percent: nil, label: nil, style: nil, gauge_style: nil, block: nil, use_unicode: true)
|
|
66
70
|
if percent
|
|
67
|
-
|
|
71
|
+
float_percent = Float(percent)
|
|
72
|
+
unless float_percent.between?(0, 100)
|
|
73
|
+
raise ArgumentError, "percent must be between 0 and 100 (got #{percent.inspect})"
|
|
74
|
+
end
|
|
75
|
+
# Float(Numeric) incorrectly returns Float? -- https://github.com/ruby/rbs/issues/2793
|
|
76
|
+
ratio = float_percent / 100.0 #: Float
|
|
68
77
|
end
|
|
69
78
|
ratio = Float(ratio || 0.0)
|
|
70
79
|
super(ratio:, label:, style:, gauge_style:, block:, use_unicode:)
|
|
@@ -72,17 +81,66 @@ module RatatuiRuby
|
|
|
72
81
|
|
|
73
82
|
# Returns true if the gauge has any fill (ratio > 0).
|
|
74
83
|
#
|
|
75
|
-
#
|
|
84
|
+
# === Example
|
|
85
|
+
#
|
|
86
|
+
#--
|
|
87
|
+
# SPDX-SnippetBegin
|
|
88
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
89
|
+
# SPDX-License-Identifier: MIT-0
|
|
90
|
+
#++
|
|
91
|
+
# Widgets::Gauge.new(ratio: 0.0).filled? # => false
|
|
92
|
+
# Widgets::Gauge.new(ratio: 0.5).filled? # => true
|
|
93
|
+
#--
|
|
94
|
+
# SPDX-SnippetEnd
|
|
95
|
+
#++
|
|
76
96
|
def filled?
|
|
77
97
|
ratio > 0
|
|
78
98
|
end
|
|
79
99
|
|
|
80
100
|
# Returns true if the gauge is at 100% or more (ratio >= 1.0).
|
|
81
101
|
#
|
|
82
|
-
#
|
|
102
|
+
# === Example
|
|
103
|
+
#
|
|
104
|
+
#--
|
|
105
|
+
# SPDX-SnippetBegin
|
|
106
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
107
|
+
# SPDX-License-Identifier: MIT-0
|
|
108
|
+
#++
|
|
109
|
+
# Widgets::Gauge.new(ratio: 0.99).complete? # => false
|
|
110
|
+
# Widgets::Gauge.new(ratio: 1.0).complete? # => true
|
|
111
|
+
#--
|
|
112
|
+
# SPDX-SnippetEnd
|
|
113
|
+
#++
|
|
83
114
|
def complete?
|
|
84
115
|
ratio >= 1.0
|
|
85
116
|
end
|
|
117
|
+
|
|
118
|
+
# Returns the progress as an integer percentage (0-100).
|
|
119
|
+
#
|
|
120
|
+
# Gauge stores progress as a ratio (0.0 to 1.0). User-facing code often
|
|
121
|
+
# displays percentages. Converting manually is tedious.
|
|
122
|
+
#
|
|
123
|
+
# This is the inverse of passing <tt>percent:</tt> to the constructor.
|
|
124
|
+
# Rounds down to the nearest integer.
|
|
125
|
+
#
|
|
126
|
+
# === Example
|
|
127
|
+
#
|
|
128
|
+
#--
|
|
129
|
+
# SPDX-SnippetBegin
|
|
130
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
131
|
+
# SPDX-License-Identifier: MIT-0
|
|
132
|
+
#++
|
|
133
|
+
# gauge = Widgets::Gauge.new(percent: 75)
|
|
134
|
+
# gauge.percent # => 75
|
|
135
|
+
#
|
|
136
|
+
# gauge = Widgets::Gauge.new(ratio: 0.456)
|
|
137
|
+
# gauge.percent # => 45
|
|
138
|
+
#--
|
|
139
|
+
# SPDX-SnippetEnd
|
|
140
|
+
#++
|
|
141
|
+
def percent
|
|
142
|
+
(ratio * 100).to_i
|
|
143
|
+
end
|
|
86
144
|
end
|
|
87
145
|
end
|
|
88
146
|
end
|
|
@@ -23,6 +23,8 @@ module RatatuiRuby
|
|
|
23
23
|
#
|
|
24
24
|
# ruby examples/widget_line_gauge/app.rb
|
|
25
25
|
class LineGauge < Data.define(:ratio, :label, :style, :filled_style, :unfilled_style, :block, :filled_symbol, :unfilled_symbol)
|
|
26
|
+
include CoerceableWidget
|
|
27
|
+
|
|
26
28
|
##
|
|
27
29
|
# :attr_reader: ratio
|
|
28
30
|
# Progress ratio from 0.0 to 1.0.
|
|
@@ -58,6 +60,7 @@ module RatatuiRuby
|
|
|
58
60
|
# Creates a new LineGauge.
|
|
59
61
|
#
|
|
60
62
|
# [ratio] Float (0.0 - 1.0).
|
|
63
|
+
# [percent] Integer (0 - 100), alternative to ratio.
|
|
61
64
|
# [label] String or Text::Span (optional).
|
|
62
65
|
# [style] Style (optional, base style for the gauge).
|
|
63
66
|
# [filled_style] Style.
|
|
@@ -65,9 +68,19 @@ module RatatuiRuby
|
|
|
65
68
|
# [block] Block.
|
|
66
69
|
# [filled_symbol] String (default: <tt>"█"</tt>).
|
|
67
70
|
# [unfilled_symbol] String (default: <tt>"░"</tt>).
|
|
68
|
-
|
|
71
|
+
#
|
|
72
|
+
# Raises ArgumentError if percent is not 0..100.
|
|
73
|
+
def initialize(ratio: nil, percent: nil, label: nil, style: nil, filled_style: nil, unfilled_style: nil, block: nil, filled_symbol: "█", unfilled_symbol: "░")
|
|
74
|
+
if percent
|
|
75
|
+
float_percent = Float(percent)
|
|
76
|
+
unless float_percent.between?(0, 100)
|
|
77
|
+
raise ArgumentError, "percent must be between 0 and 100 (got #{percent.inspect})"
|
|
78
|
+
end
|
|
79
|
+
ratio = float_percent / 100.0
|
|
80
|
+
end
|
|
81
|
+
ratio = Float(ratio || 0.0)
|
|
69
82
|
super(
|
|
70
|
-
ratio
|
|
83
|
+
ratio:,
|
|
71
84
|
label:,
|
|
72
85
|
style:,
|
|
73
86
|
filled_style:,
|
|
@@ -80,17 +93,66 @@ module RatatuiRuby
|
|
|
80
93
|
|
|
81
94
|
# Returns true if the gauge has any fill (ratio > 0).
|
|
82
95
|
#
|
|
83
|
-
#
|
|
96
|
+
# === Example
|
|
97
|
+
#
|
|
98
|
+
#--
|
|
99
|
+
# SPDX-SnippetBegin
|
|
100
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
101
|
+
# SPDX-License-Identifier: MIT-0
|
|
102
|
+
#++
|
|
103
|
+
# Widgets::LineGauge.new(ratio: 0.0).filled? # => false
|
|
104
|
+
# Widgets::LineGauge.new(ratio: 0.5).filled? # => true
|
|
105
|
+
#--
|
|
106
|
+
# SPDX-SnippetEnd
|
|
107
|
+
#++
|
|
84
108
|
def filled?
|
|
85
109
|
ratio > 0
|
|
86
110
|
end
|
|
87
111
|
|
|
88
112
|
# Returns true if the gauge is at 100% or more (ratio >= 1.0).
|
|
89
113
|
#
|
|
90
|
-
#
|
|
114
|
+
# === Example
|
|
115
|
+
#
|
|
116
|
+
#--
|
|
117
|
+
# SPDX-SnippetBegin
|
|
118
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
119
|
+
# SPDX-License-Identifier: MIT-0
|
|
120
|
+
#++
|
|
121
|
+
# Widgets::LineGauge.new(ratio: 0.99).complete? # => false
|
|
122
|
+
# Widgets::LineGauge.new(ratio: 1.0).complete? # => true
|
|
123
|
+
#--
|
|
124
|
+
# SPDX-SnippetEnd
|
|
125
|
+
#++
|
|
91
126
|
def complete?
|
|
92
127
|
ratio >= 1.0
|
|
93
128
|
end
|
|
129
|
+
|
|
130
|
+
# Returns the progress as an integer percentage (0-100).
|
|
131
|
+
#
|
|
132
|
+
# LineGauge stores progress as a ratio (0.0 to 1.0). User-facing code often
|
|
133
|
+
# displays percentages. Converting manually is tedious.
|
|
134
|
+
#
|
|
135
|
+
# This is the inverse of passing <tt>percent:</tt> to the constructor.
|
|
136
|
+
# Rounds down to the nearest integer.
|
|
137
|
+
#
|
|
138
|
+
# === Example
|
|
139
|
+
#
|
|
140
|
+
#--
|
|
141
|
+
# SPDX-SnippetBegin
|
|
142
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
143
|
+
# SPDX-License-Identifier: MIT-0
|
|
144
|
+
#++
|
|
145
|
+
# lg = Widgets::LineGauge.new(percent: 75)
|
|
146
|
+
# lg.percent # => 75
|
|
147
|
+
#
|
|
148
|
+
# lg = Widgets::LineGauge.new(ratio: 0.456)
|
|
149
|
+
# lg.percent # => 45
|
|
150
|
+
#--
|
|
151
|
+
# SPDX-SnippetEnd
|
|
152
|
+
#++
|
|
153
|
+
def percent
|
|
154
|
+
(ratio * 100).to_i
|
|
155
|
+
end
|
|
94
156
|
end
|
|
95
157
|
end
|
|
96
158
|
end
|
|
@@ -39,6 +39,8 @@ module RatatuiRuby
|
|
|
39
39
|
# SPDX-SnippetEnd
|
|
40
40
|
#++
|
|
41
41
|
class List < Data.define(:items, :selected_index, :offset, :style, :highlight_style, :highlight_symbol, :repeat_highlight_symbol, :highlight_spacing, :direction, :scroll_padding, :block)
|
|
42
|
+
include CoerceableWidget
|
|
43
|
+
|
|
42
44
|
##
|
|
43
45
|
# Highlight spacing: always show the spacing column.
|
|
44
46
|
HIGHLIGHT_ALWAYS = :always
|
|
@@ -152,17 +154,99 @@ module RatatuiRuby
|
|
|
152
154
|
|
|
153
155
|
# Returns true if an item is selected.
|
|
154
156
|
#
|
|
155
|
-
#
|
|
157
|
+
# === Example
|
|
158
|
+
#
|
|
159
|
+
#--
|
|
160
|
+
# SPDX-SnippetBegin
|
|
161
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
162
|
+
# SPDX-License-Identifier: MIT-0
|
|
163
|
+
#++
|
|
164
|
+
# list = Widgets::List.new(items: %w[a b c])
|
|
165
|
+
# list.selected? # => false
|
|
166
|
+
#
|
|
167
|
+
# list = Widgets::List.new(items: %w[a b c], selected_index: 1)
|
|
168
|
+
# list.selected? # => true
|
|
169
|
+
#
|
|
170
|
+
#--
|
|
171
|
+
# SPDX-SnippetEnd
|
|
172
|
+
#++
|
|
173
|
+
# Returns: Boolean.
|
|
156
174
|
def selected?
|
|
157
175
|
!selected_index.nil?
|
|
158
176
|
end
|
|
159
177
|
|
|
160
|
-
# Returns true if the list
|
|
178
|
+
# Returns true if the list contains no items.
|
|
179
|
+
#
|
|
180
|
+
# === Example
|
|
161
181
|
#
|
|
162
|
-
|
|
182
|
+
#--
|
|
183
|
+
# SPDX-SnippetBegin
|
|
184
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
185
|
+
# SPDX-License-Identifier: MIT-0
|
|
186
|
+
#++
|
|
187
|
+
# list = Widgets::List.new(items: [])
|
|
188
|
+
# list.empty? # => true
|
|
189
|
+
#
|
|
190
|
+
#--
|
|
191
|
+
# SPDX-SnippetEnd
|
|
192
|
+
#++
|
|
193
|
+
# Returns: Boolean.
|
|
163
194
|
def empty?
|
|
164
195
|
items.empty?
|
|
165
196
|
end
|
|
197
|
+
|
|
198
|
+
# Returns the number of items in the list.
|
|
199
|
+
#
|
|
200
|
+
# === Example
|
|
201
|
+
#
|
|
202
|
+
#--
|
|
203
|
+
# SPDX-SnippetBegin
|
|
204
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
205
|
+
# SPDX-License-Identifier: MIT-0
|
|
206
|
+
#++
|
|
207
|
+
# list = Widgets::List.new(items: %w[alpha beta gamma])
|
|
208
|
+
# list.len # => 3
|
|
209
|
+
#
|
|
210
|
+
#--
|
|
211
|
+
# SPDX-SnippetEnd
|
|
212
|
+
#++
|
|
213
|
+
# Returns: Integer.
|
|
214
|
+
def len
|
|
215
|
+
items.length
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
alias length len
|
|
219
|
+
alias size len
|
|
220
|
+
|
|
221
|
+
# NOTE: No 'selection' alias - it's ambiguous whether it returns an item or index.
|
|
222
|
+
# Use selected_index for the index, selected_item for the item.
|
|
223
|
+
|
|
224
|
+
# Returns the currently selected item, or nil if nothing is selected.
|
|
225
|
+
#
|
|
226
|
+
# Accessing the selected item directly requires looking up +items[selected_index]+
|
|
227
|
+
# after checking that +selected_index+ is not nil. This is verbose.
|
|
228
|
+
#
|
|
229
|
+
# This method encapsulates that pattern.
|
|
230
|
+
#
|
|
231
|
+
# === Example
|
|
232
|
+
#
|
|
233
|
+
#--
|
|
234
|
+
# SPDX-SnippetBegin
|
|
235
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
236
|
+
# SPDX-License-Identifier: MIT-0
|
|
237
|
+
#++
|
|
238
|
+
# list = Widgets::List.new(items: %w[alpha beta gamma], selected_index: 1)
|
|
239
|
+
# list.selected_item # => "beta"
|
|
240
|
+
#
|
|
241
|
+
#--
|
|
242
|
+
# SPDX-SnippetEnd
|
|
243
|
+
#++
|
|
244
|
+
# Returns: The item at the selected index, or nil if no selection.
|
|
245
|
+
def selected_item
|
|
246
|
+
return nil if selected_index.nil?
|
|
247
|
+
|
|
248
|
+
items[selected_index]
|
|
249
|
+
end
|
|
166
250
|
end
|
|
167
251
|
end
|
|
168
252
|
end
|
|
@@ -15,6 +15,8 @@ module RatatuiRuby
|
|
|
15
15
|
#
|
|
16
16
|
# Use it for everything from simple labels to complex, multi-paragraph documents.
|
|
17
17
|
#
|
|
18
|
+
# See also: <tt>examples/widget_scroll_text/app.rb</tt> for scrollable paragraphs.
|
|
19
|
+
#
|
|
18
20
|
# === Examples
|
|
19
21
|
#
|
|
20
22
|
#--
|
|
@@ -39,6 +41,8 @@ module RatatuiRuby
|
|
|
39
41
|
# SPDX-SnippetEnd
|
|
40
42
|
#++
|
|
41
43
|
class Paragraph < Data.define(:text, :style, :block, :wrap, :alignment, :scroll)
|
|
44
|
+
include CoerceableWidget
|
|
45
|
+
|
|
42
46
|
##
|
|
43
47
|
# :attr_reader: text
|
|
44
48
|
# The content to display.
|
|
@@ -37,6 +37,8 @@ module RatatuiRuby
|
|
|
37
37
|
# SPDX-SnippetEnd
|
|
38
38
|
#++
|
|
39
39
|
class Row < Data.define(:cells, :style, :height, :top_margin, :bottom_margin)
|
|
40
|
+
include CoerceableWidget
|
|
41
|
+
|
|
40
42
|
##
|
|
41
43
|
# :attr_reader: cells
|
|
42
44
|
# The cells to display (Array of Strings, Text::Spans, Text::Lines, Paragraphs, or Cells).
|
|
@@ -73,6 +75,49 @@ module RatatuiRuby
|
|
|
73
75
|
bottom_margin: bottom_margin.nil? ? nil : Integer(bottom_margin)
|
|
74
76
|
)
|
|
75
77
|
end
|
|
78
|
+
|
|
79
|
+
# Returns a new Row with strikethrough styling enabled.
|
|
80
|
+
#
|
|
81
|
+
# Table rows sometimes need strikethrough styling to indicate
|
|
82
|
+
# deleted, deprecated, or cancelled items. Manually managing
|
|
83
|
+
# style modifiers is tedious.
|
|
84
|
+
#
|
|
85
|
+
# This method adds the <tt>:crossed_out</tt> modifier to the row's
|
|
86
|
+
# style. If the row has no existing style, a new style is created.
|
|
87
|
+
#
|
|
88
|
+
# Use it to visually mark rows as cancelled, completed, or invalid.
|
|
89
|
+
#
|
|
90
|
+
# *Terminal Compatibility:* Strikethrough (SGR 9) is not universally
|
|
91
|
+
# supported. macOS Terminal.app notably lacks support. Modern terminals
|
|
92
|
+
# like Kitty, iTerm2, Alacritty, and WezTerm render it correctly.
|
|
93
|
+
# Consider pairing with <tt>:dim</tt> as a fallback for visibility.
|
|
94
|
+
#
|
|
95
|
+
# Returns a new Row instance with strikethrough enabled.
|
|
96
|
+
#
|
|
97
|
+
# === Example
|
|
98
|
+
#
|
|
99
|
+
#--
|
|
100
|
+
# SPDX-SnippetBegin
|
|
101
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
102
|
+
# SPDX-License-Identifier: MIT-0
|
|
103
|
+
#++
|
|
104
|
+
# row = Row.new(cells: ["Cancelled Task", "2024-01-15"])
|
|
105
|
+
# strikethrough_row = row.enable_strikethrough
|
|
106
|
+
# # Row style now includes :crossed_out modifier
|
|
107
|
+
#--
|
|
108
|
+
# SPDX-SnippetEnd
|
|
109
|
+
#++
|
|
110
|
+
def enable_strikethrough
|
|
111
|
+
current_style = style || Style::Style.new
|
|
112
|
+
current_modifiers = current_style.modifiers || []
|
|
113
|
+
new_modifiers = current_modifiers.include?(:crossed_out) ? current_modifiers : current_modifiers + [:crossed_out]
|
|
114
|
+
|
|
115
|
+
new_style = current_style.with(modifiers: new_modifiers)
|
|
116
|
+
with(style: new_style)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Ruby-idiomatic alias
|
|
120
|
+
alias strikethrough enable_strikethrough
|
|
76
121
|
end
|
|
77
122
|
end
|
|
78
123
|
end
|