ratatui_ruby 0.5.0 → 0.6.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 +6 -0
- data/CHANGELOG.md +44 -7
- data/README.md +11 -4
- data/REUSE.toml +2 -7
- data/doc/application_architecture.md +84 -10
- data/doc/application_testing.md +75 -29
- data/doc/contributors/design/ruby_frontend.md +39 -3
- data/doc/contributors/design/rust_backend.md +1 -0
- data/doc/contributors/developing_examples.md +129 -44
- data/doc/contributors/examples_audit/p1_high.md +21 -0
- data/doc/contributors/examples_audit/p2_moderate.md +81 -0
- data/doc/contributors/examples_audit.md +41 -0
- data/doc/event_handling.md +11 -3
- data/doc/images/app_all_events.png +0 -0
- data/doc/images/app_color_picker.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_demo.png +0 -0
- data/doc/images/widget_block_demo.png +0 -0
- data/doc/images/widget_canvas_demo.png +0 -0
- data/doc/images/widget_cell_demo.png +0 -0
- data/doc/images/widget_center_demo.png +0 -0
- data/doc/images/widget_chart_demo.png +0 -0
- data/doc/images/widget_list_demo.png +0 -0
- data/doc/images/widget_overlay_demo.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_sparkline_demo.png +0 -0
- data/doc/images/widget_table_demo.png +0 -0
- data/doc/images/widget_tabs_demo.png +0 -0
- data/doc/images/widget_text_width.png +0 -0
- data/doc/quickstart.md +69 -76
- data/doc/terminal_limitations.md +92 -0
- data/examples/app_all_events/README.md +45 -27
- data/examples/app_all_events/app.rb +38 -35
- data/examples/app_all_events/model/app_model.rb +157 -0
- data/examples/app_all_events/model/event_entry.rb +17 -0
- data/examples/app_all_events/model/msg.rb +37 -0
- data/examples/app_all_events/update.rb +73 -0
- data/examples/app_all_events/view/app_view.rb +8 -8
- data/examples/app_all_events/view/controls_view.rb +8 -6
- data/examples/app_all_events/view/counts_view.rb +12 -8
- data/examples/app_all_events/view/live_view.rb +8 -7
- data/examples/app_all_events/view/log_view.rb +10 -15
- data/examples/app_color_picker/README.md +84 -44
- data/examples/app_color_picker/app.rb +24 -62
- data/examples/app_color_picker/controls.rb +90 -0
- data/examples/app_color_picker/copy_dialog.rb +45 -49
- data/examples/app_color_picker/export_pane.rb +126 -0
- data/examples/app_color_picker/input.rb +99 -67
- data/examples/app_color_picker/main_container.rb +178 -0
- data/examples/app_color_picker/palette.rb +55 -26
- data/examples/app_login_form/README.md +47 -0
- data/examples/app_login_form/app.rb +2 -3
- data/examples/app_stateful_interaction/README.md +31 -0
- data/examples/app_stateful_interaction/app.rb +272 -0
- data/examples/timeout_demo.rb +43 -0
- data/examples/verify_quickstart_dsl/README.md +48 -0
- data/examples/verify_quickstart_dsl/app.rb +2 -0
- data/examples/verify_quickstart_layout/README.md +71 -0
- data/examples/verify_quickstart_layout/app.rb +2 -0
- data/examples/verify_quickstart_lifecycle/README.md +56 -0
- data/examples/verify_quickstart_lifecycle/app.rb +8 -2
- data/examples/verify_readme_usage/README.md +43 -0
- data/examples/verify_readme_usage/app.rb +8 -2
- data/examples/widget_barchart_demo/README.md +49 -0
- data/examples/widget_barchart_demo/app.rb +5 -5
- data/examples/widget_block_demo/README.md +34 -0
- data/examples/widget_block_demo/app.rb +256 -0
- data/examples/widget_box_demo/README.md +45 -0
- data/examples/widget_calendar_demo/README.md +39 -0
- data/examples/widget_canvas_demo/README.md +27 -0
- data/examples/widget_canvas_demo/app.rb +123 -0
- data/examples/widget_cell_demo/README.md +36 -0
- data/examples/widget_cell_demo/app.rb +31 -24
- data/examples/widget_center_demo/README.md +29 -0
- data/examples/widget_center_demo/app.rb +116 -0
- data/examples/widget_chart_demo/README.md +41 -0
- data/examples/widget_chart_demo/app.rb +7 -2
- data/examples/widget_gauge_demo/README.md +41 -0
- data/examples/widget_layout_split/README.md +44 -0
- data/examples/widget_line_gauge_demo/README.md +41 -0
- data/examples/widget_list_demo/README.md +49 -0
- data/examples/widget_list_demo/app.rb +91 -107
- data/examples/widget_map_demo/README.md +39 -0
- data/examples/{app_map_demo → widget_map_demo}/app.rb +2 -2
- data/examples/widget_overlay_demo/app.rb +248 -0
- data/examples/widget_popup_demo/README.md +36 -0
- data/examples/widget_ratatui_logo_demo/README.md +34 -0
- data/examples/widget_ratatui_mascot_demo/README.md +34 -0
- data/examples/widget_rect/README.md +38 -0
- data/examples/widget_render/README.md +37 -0
- data/examples/widget_rich_text/README.md +35 -0
- data/examples/widget_rich_text/app.rb +62 -33
- data/examples/widget_scroll_text/README.md +37 -0
- data/examples/widget_scroll_text/app.rb +0 -1
- data/examples/widget_scrollbar_demo/README.md +37 -0
- data/examples/widget_sparkline_demo/README.md +42 -0
- data/examples/widget_sparkline_demo/app.rb +4 -3
- data/examples/widget_style_colors/README.md +34 -0
- data/examples/widget_table_demo/README.md +48 -0
- data/examples/{app_table_select → widget_table_demo}/app.rb +46 -8
- data/examples/widget_tabs_demo/README.md +41 -0
- data/examples/widget_tabs_demo/app.rb +15 -1
- data/examples/widget_text_width/README.md +35 -0
- data/examples/widget_text_width/app.rb +106 -0
- data/exe/.gitkeep +0 -0
- data/ext/ratatui_ruby/Cargo.lock +11 -4
- data/ext/ratatui_ruby/Cargo.toml +2 -1
- data/ext/ratatui_ruby/src/events.rs +238 -26
- data/ext/ratatui_ruby/src/frame.rs +113 -1
- data/ext/ratatui_ruby/src/lib.rs +34 -4
- data/ext/ratatui_ruby/src/string_width.rs +101 -0
- data/ext/ratatui_ruby/src/terminal.rs +39 -15
- data/ext/ratatui_ruby/src/text.rs +1 -1
- data/ext/ratatui_ruby/src/widgets/barchart.rs +24 -6
- data/ext/ratatui_ruby/src/widgets/gauge.rs +9 -2
- data/ext/ratatui_ruby/src/widgets/line_gauge.rs +9 -2
- data/ext/ratatui_ruby/src/widgets/list.rs +179 -3
- data/ext/ratatui_ruby/src/widgets/list_state.rs +137 -0
- data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
- data/ext/ratatui_ruby/src/widgets/scrollbar.rs +93 -1
- data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +169 -0
- data/ext/ratatui_ruby/src/widgets/table.rs +113 -1
- data/ext/ratatui_ruby/src/widgets/table_state.rs +121 -0
- data/lib/ratatui_ruby/cell.rb +4 -4
- data/lib/ratatui_ruby/event/key/character.rb +35 -0
- data/lib/ratatui_ruby/event/key/media.rb +44 -0
- data/lib/ratatui_ruby/event/key/modifier.rb +95 -0
- data/lib/ratatui_ruby/event/key/navigation.rb +55 -0
- data/lib/ratatui_ruby/event/key/system.rb +45 -0
- data/lib/ratatui_ruby/event/key.rb +111 -51
- data/lib/ratatui_ruby/event/mouse.rb +3 -3
- data/lib/ratatui_ruby/event/paste.rb +1 -1
- data/lib/ratatui_ruby/frame.rb +96 -0
- data/lib/ratatui_ruby/list_state.rb +88 -0
- data/lib/ratatui_ruby/schema/bar_chart/bar.rb +2 -2
- data/lib/ratatui_ruby/schema/cursor.rb +5 -0
- data/lib/ratatui_ruby/schema/gauge.rb +3 -1
- data/lib/ratatui_ruby/schema/line_gauge.rb +2 -2
- data/lib/ratatui_ruby/schema/list.rb +25 -4
- data/lib/ratatui_ruby/schema/list_item.rb +41 -0
- data/lib/ratatui_ruby/schema/rect.rb +43 -0
- data/lib/ratatui_ruby/schema/style.rb +24 -4
- data/lib/ratatui_ruby/schema/table.rb +21 -3
- data/lib/ratatui_ruby/schema/text.rb +69 -1
- data/lib/ratatui_ruby/scrollbar_state.rb +112 -0
- data/lib/ratatui_ruby/session/autodoc.rb +65 -0
- data/lib/ratatui_ruby/session.rb +22 -7
- data/lib/ratatui_ruby/table_state.rb +90 -0
- data/lib/ratatui_ruby/test_helper/event_injection.rb +169 -0
- data/lib/ratatui_ruby/test_helper/snapshot.rb +390 -0
- data/lib/ratatui_ruby/test_helper/style_assertions.rb +351 -0
- data/lib/ratatui_ruby/test_helper/terminal.rb +127 -0
- data/lib/ratatui_ruby/test_helper/test_doubles.rb +68 -0
- data/lib/ratatui_ruby/test_helper.rb +65 -358
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby.rb +42 -19
- data/sig/examples/app_stateful_interaction/app.rbs +33 -0
- data/sig/examples/widget_block_demo/app.rbs +32 -0
- data/sig/examples/{app_map_demo → widget_map_demo}/app.rbs +2 -2
- data/sig/examples/{app_table_select → widget_table_demo}/app.rbs +2 -2
- data/sig/examples/{widget_table_flex → widget_text_width}/app.rbs +2 -3
- data/sig/ratatui_ruby/event.rbs +11 -1
- data/sig/ratatui_ruby/frame.rbs +2 -0
- data/sig/ratatui_ruby/list_state.rbs +13 -0
- data/sig/ratatui_ruby/ratatui_ruby.rbs +2 -2
- data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +3 -3
- data/sig/ratatui_ruby/schema/gauge.rbs +2 -2
- data/sig/ratatui_ruby/schema/line_gauge.rbs +2 -2
- data/sig/ratatui_ruby/schema/list.rbs +4 -2
- data/sig/ratatui_ruby/schema/list_item.rbs +10 -0
- data/sig/ratatui_ruby/schema/rect.rbs +3 -0
- data/sig/ratatui_ruby/schema/style.rbs +3 -3
- data/sig/ratatui_ruby/schema/table.rbs +3 -1
- data/sig/ratatui_ruby/schema/text.rbs +8 -6
- data/sig/ratatui_ruby/scrollbar_state.rbs +18 -0
- data/sig/ratatui_ruby/session.rbs +13 -0
- data/sig/ratatui_ruby/table_state.rbs +15 -0
- data/sig/ratatui_ruby/test_helper/event_injection.rbs +16 -0
- data/sig/ratatui_ruby/test_helper/snapshot.rbs +12 -0
- data/sig/ratatui_ruby/test_helper/style_assertions.rbs +64 -0
- data/sig/ratatui_ruby/test_helper/terminal.rbs +14 -0
- data/sig/ratatui_ruby/test_helper/test_doubles.rbs +22 -0
- data/sig/ratatui_ruby/test_helper.rbs +5 -4
- data/tasks/autodoc/examples.rb +79 -0
- data/tasks/autodoc/inventory.rb +9 -7
- data/tasks/autodoc.rake +11 -5
- data/tasks/bump/changelog.rb +3 -3
- data/tasks/bump/links.rb +67 -0
- data/tasks/sourcehut.rake +61 -21
- data/tasks/terminal_preview/app_screenshot.rb +13 -3
- data/tasks/terminal_preview/saved_screenshot.rb +4 -3
- metadata +111 -37
- data/doc/images/app_table_select.png +0 -0
- data/doc/images/widget_block_padding.png +0 -0
- data/doc/images/widget_block_titles.png +0 -0
- data/doc/images/widget_list_styles.png +0 -0
- data/examples/app_all_events/model/events.rb +0 -180
- data/examples/app_all_events/model/highlight.rb +0 -57
- data/examples/app_all_events/test/snapshots/after_focus_lost.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_focus_regained.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_horizontal_resize.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_key_a.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_key_ctrl_x.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_mouse_click.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_mouse_drag.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_multiple_events.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_paste.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_resize.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_right_click.txt +0 -24
- data/examples/app_all_events/test/snapshots/after_vertical_resize.txt +0 -24
- data/examples/app_all_events/test/snapshots/initial_state.txt +0 -24
- data/examples/app_all_events/view_state.rb +0 -42
- data/examples/app_color_picker/scene.rb +0 -201
- data/examples/widget_block_padding/app.rb +0 -67
- data/examples/widget_block_titles/app.rb +0 -69
- data/examples/widget_list_styles/app.rb +0 -141
- data/examples/widget_table_flex/app.rb +0 -95
- data/sig/examples/widget_block_padding/app.rbs +0 -11
- data/sig/examples/widget_block_titles/app.rbs +0 -11
- data/sig/examples/widget_list_styles/app.rbs +0 -11
- data/tasks/bump/comparison_links.rb +0 -41
- /data/doc/images/{app_map_demo.png → widget_map_demo.png} +0 -0
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "timeout"
|
|
4
|
-
require "minitest/mock"
|
|
5
3
|
require "fileutils"
|
|
4
|
+
require_relative "test_helper/terminal"
|
|
5
|
+
require_relative "test_helper/snapshot"
|
|
6
|
+
require_relative "test_helper/event_injection"
|
|
7
|
+
require_relative "test_helper/style_assertions"
|
|
8
|
+
require_relative "test_helper/test_doubles"
|
|
6
9
|
|
|
7
10
|
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
8
11
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
@@ -11,374 +14,78 @@ module RatatuiRuby
|
|
|
11
14
|
##
|
|
12
15
|
# Helpers for testing RatatuiRuby applications.
|
|
13
16
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
17
|
+
# Writing TUI tests by hand is tedious. You need a headless terminal, event
|
|
18
|
+
# injection, snapshot comparisons, and style assertions. Wiring all that up
|
|
19
|
+
# yourself is error-prone.
|
|
16
20
|
#
|
|
17
|
-
#
|
|
21
|
+
# This module bundles everything you need. Include it in your test class and
|
|
22
|
+
# start writing tests immediately.
|
|
23
|
+
#
|
|
24
|
+
# == Included Mixins
|
|
25
|
+
#
|
|
26
|
+
# [Terminal] Sets up a headless terminal and queries its buffer.
|
|
27
|
+
# [Snapshot] Compares the screen against stored reference files.
|
|
28
|
+
# [EventInjection] Simulates keypresses, mouse clicks, and resize events.
|
|
29
|
+
# [StyleAssertions] Checks foreground color, background color, and text modifiers.
|
|
30
|
+
# [TestDoubles] Provides mocks and stubs for testing views in isolation.
|
|
31
|
+
#
|
|
32
|
+
# == Example
|
|
18
33
|
#
|
|
19
34
|
# require "ratatui_ruby/test_helper"
|
|
20
35
|
#
|
|
21
|
-
# class
|
|
36
|
+
# class TestMyApp < Minitest::Test
|
|
22
37
|
# include RatatuiRuby::TestHelper
|
|
23
38
|
#
|
|
24
|
-
# def
|
|
39
|
+
# def test_initial_render
|
|
25
40
|
# with_test_terminal(80, 24) do
|
|
26
|
-
#
|
|
27
|
-
#
|
|
41
|
+
# MyApp.new.run_once
|
|
42
|
+
# assert_snapshot("initial")
|
|
43
|
+
# end
|
|
44
|
+
# end
|
|
45
|
+
#
|
|
46
|
+
# def test_themes
|
|
47
|
+
# with_test_terminal do
|
|
48
|
+
# app = ThemeDemo.new
|
|
49
|
+
# app.run_once
|
|
50
|
+
# assert_rich_snapshot("default_theme")
|
|
51
|
+
#
|
|
52
|
+
# inject_key("t", modifiers: [:ctrl])
|
|
53
|
+
# app.run_once
|
|
54
|
+
# assert_rich_snapshot("dark_theme")
|
|
55
|
+
#
|
|
56
|
+
# inject_key("t", modifiers: [:ctrl])
|
|
57
|
+
# app.run_once
|
|
58
|
+
# assert_rich_snapshot("high_contrast_theme")
|
|
59
|
+
# end
|
|
60
|
+
# end
|
|
61
|
+
#
|
|
62
|
+
# def test_highlighter_applies_selection_style
|
|
63
|
+
# with_test_terminal(40, 5) do
|
|
64
|
+
# RatatuiRuby.draw do |frame|
|
|
65
|
+
# highlighter = MyApp::UI::Highlighter.new(:yellow)
|
|
66
|
+
# highlighter.render_at(frame, 0, 2, "Selected Item")
|
|
67
|
+
# end
|
|
68
|
+
#
|
|
69
|
+
# assert_fg_color(:yellow, 0, 2)
|
|
70
|
+
# assert_bold(0, 2)
|
|
28
71
|
# end
|
|
29
72
|
# end
|
|
30
73
|
#
|
|
31
|
-
# def
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
74
|
+
# def test_view_in_isolation
|
|
75
|
+
# frame = MockFrame.new
|
|
76
|
+
# area = StubRect.new(width: 60, height: 20)
|
|
77
|
+
#
|
|
78
|
+
# MyView.new.call(state, tui, frame, area)
|
|
79
|
+
#
|
|
80
|
+
# widget = frame.rendered_widgets.first[:widget]
|
|
81
|
+
# assert_equal "Dashboard", widget.block.title
|
|
35
82
|
# end
|
|
36
83
|
# end
|
|
37
84
|
module TestHelper
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
# +height+:: height of the test terminal (default: 24)
|
|
44
|
-
#
|
|
45
|
-
# +timeout+:: maximum execution time in seconds (default: 2). Pass nil to disable.
|
|
46
|
-
#
|
|
47
|
-
# If a block is given, it is executed within the test terminal context.
|
|
48
|
-
def with_test_terminal(width = 80, height = 24, **opts)
|
|
49
|
-
RatatuiRuby.init_test_terminal(width, height)
|
|
50
|
-
# Flush any lingering events from previous tests
|
|
51
|
-
while (event = RatatuiRuby.poll_event) && !event.none?; end
|
|
52
|
-
|
|
53
|
-
RatatuiRuby.stub :init_terminal, nil do
|
|
54
|
-
RatatuiRuby.stub :restore_terminal, nil do
|
|
55
|
-
@_ratatui_test_terminal_active = true
|
|
56
|
-
timeout = opts.fetch(:timeout, 2)
|
|
57
|
-
if timeout
|
|
58
|
-
Timeout.timeout(timeout) do
|
|
59
|
-
yield
|
|
60
|
-
end
|
|
61
|
-
else
|
|
62
|
-
yield
|
|
63
|
-
end
|
|
64
|
-
ensure
|
|
65
|
-
@_ratatui_test_terminal_active = false
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
ensure
|
|
69
|
-
RatatuiRuby.restore_terminal
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
##
|
|
73
|
-
# Returns the current content of the terminal buffer as an array of strings.
|
|
74
|
-
# Each string represents a row in the terminal.
|
|
75
|
-
#
|
|
76
|
-
# buffer_content
|
|
77
|
-
# # => ["Row 1 text", "Row 2 text", ...]
|
|
78
|
-
def buffer_content
|
|
79
|
-
RatatuiRuby.get_buffer_content.split("\n")
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
##
|
|
83
|
-
# Returns the current cursor position as a hash with +:x+ and +:y+ keys.
|
|
84
|
-
#
|
|
85
|
-
# cursor_position
|
|
86
|
-
# # => { x: 0, y: 0 }
|
|
87
|
-
def cursor_position
|
|
88
|
-
x, y = RatatuiRuby.get_cursor_position
|
|
89
|
-
{ x:, y: }
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
##
|
|
93
|
-
# Injects an event into the event queue for testing.
|
|
94
|
-
#
|
|
95
|
-
# Pass any RatatuiRuby::Event object. The event will be returned by
|
|
96
|
-
# the next call to RatatuiRuby.poll_event.
|
|
97
|
-
#
|
|
98
|
-
# Raises a +RuntimeError+ if called outside of a +with_test_terminal+ block.
|
|
99
|
-
#
|
|
100
|
-
# == Examples
|
|
101
|
-
#
|
|
102
|
-
# with_test_terminal do
|
|
103
|
-
# # Key events
|
|
104
|
-
# inject_event(RatatuiRuby::Event::Key.new(code: "q"))
|
|
105
|
-
# inject_event(RatatuiRuby::Event::Key.new(code: "s", modifiers: ["ctrl"]))
|
|
106
|
-
#
|
|
107
|
-
# # Mouse events
|
|
108
|
-
# inject_event(RatatuiRuby::Event::Mouse.new(kind: "down", button: "left", x: 10, y: 5))
|
|
109
|
-
#
|
|
110
|
-
# # Resize events
|
|
111
|
-
# inject_event(RatatuiRuby::Event::Resize.new(width: 120, height: 40))
|
|
112
|
-
#
|
|
113
|
-
# # Paste events
|
|
114
|
-
# inject_event(RatatuiRuby::Event::Paste.new(content: "Hello"))
|
|
115
|
-
#
|
|
116
|
-
# # Focus events
|
|
117
|
-
# inject_event(RatatuiRuby::Event::FocusGained.new)
|
|
118
|
-
# inject_event(RatatuiRuby::Event::FocusLost.new)
|
|
119
|
-
# end
|
|
120
|
-
def inject_event(event)
|
|
121
|
-
unless @_ratatui_test_terminal_active
|
|
122
|
-
raise "Events must be injected inside a `with_test_terminal` block. " \
|
|
123
|
-
"Calling this method outside the block causes a race condition where the event " \
|
|
124
|
-
"is flushed before the application starts."
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
case event
|
|
128
|
-
when RatatuiRuby::Event::Key
|
|
129
|
-
RatatuiRuby.inject_test_event("key", { code: event.code, modifiers: event.modifiers })
|
|
130
|
-
when RatatuiRuby::Event::Mouse
|
|
131
|
-
RatatuiRuby.inject_test_event("mouse", {
|
|
132
|
-
kind: event.kind,
|
|
133
|
-
button: event.button,
|
|
134
|
-
x: event.x,
|
|
135
|
-
y: event.y,
|
|
136
|
-
modifiers: event.modifiers,
|
|
137
|
-
})
|
|
138
|
-
when RatatuiRuby::Event::Resize
|
|
139
|
-
RatatuiRuby.inject_test_event("resize", { width: event.width, height: event.height })
|
|
140
|
-
when RatatuiRuby::Event::Paste
|
|
141
|
-
RatatuiRuby.inject_test_event("paste", { content: event.content })
|
|
142
|
-
when RatatuiRuby::Event::FocusGained
|
|
143
|
-
RatatuiRuby.inject_test_event("focus_gained", {})
|
|
144
|
-
when RatatuiRuby::Event::FocusLost
|
|
145
|
-
RatatuiRuby.inject_test_event("focus_lost", {})
|
|
146
|
-
else
|
|
147
|
-
raise ArgumentError, "Unknown event type: #{event.class}"
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
##
|
|
152
|
-
# Injects a mouse event.
|
|
153
|
-
#
|
|
154
|
-
# inject_mouse(x: 10, y: 5, kind: :down, button: :left)
|
|
155
|
-
def inject_mouse(x:, y:, kind: :down, modifiers: [], button: :left)
|
|
156
|
-
event = RatatuiRuby::Event::Mouse.new(
|
|
157
|
-
kind: kind.to_s,
|
|
158
|
-
x:,
|
|
159
|
-
y:,
|
|
160
|
-
button: button.to_s,
|
|
161
|
-
modifiers:
|
|
162
|
-
)
|
|
163
|
-
inject_event(event)
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
##
|
|
167
|
-
# Injects a mouse left click (down) event.
|
|
168
|
-
#
|
|
169
|
-
# inject_click(x: 10, y: 5)
|
|
170
|
-
def inject_click(x:, y:, modifiers: [])
|
|
171
|
-
inject_mouse(x:, y:, kind: :down, modifiers:, button: :left)
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
##
|
|
175
|
-
# Injects a mouse right click (down) event.
|
|
176
|
-
#
|
|
177
|
-
# inject_right_click(x: 10, y: 5)
|
|
178
|
-
def inject_right_click(x:, y:, modifiers: [])
|
|
179
|
-
inject_mouse(x:, y:, kind: :down, modifiers:, button: :right)
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
##
|
|
183
|
-
# Injects a mouse drag event.
|
|
184
|
-
#
|
|
185
|
-
# inject_drag(x: 10, y: 5)
|
|
186
|
-
def inject_drag(x:, y:, modifiers: [], button: :left)
|
|
187
|
-
inject_mouse(x:, y:, kind: :drag, modifiers:, button:)
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
##
|
|
191
|
-
# Injects multiple Key events into the queue.
|
|
192
|
-
#
|
|
193
|
-
# Supports multiple formats for convenience:
|
|
194
|
-
#
|
|
195
|
-
# * String: Converted to a Key event with that code.
|
|
196
|
-
# * Symbol: Parsed as modifier_code (e.g., <tt>:ctrl_c</tt>, <tt>:enter</tt>).
|
|
197
|
-
# * Hash: Passed to Key.new constructor.
|
|
198
|
-
# * Key: Passed directly.
|
|
199
|
-
#
|
|
200
|
-
# == Examples
|
|
201
|
-
#
|
|
202
|
-
# with_test_terminal do
|
|
203
|
-
# inject_keys("a", "b", "c")
|
|
204
|
-
# inject_keys(:enter, :esc)
|
|
205
|
-
# inject_keys(:ctrl_c, :alt_shift_left)
|
|
206
|
-
# inject_keys("j", { code: "k", modifiers: ["ctrl"] })
|
|
207
|
-
# end
|
|
208
|
-
def inject_keys(*args)
|
|
209
|
-
args.each do |arg|
|
|
210
|
-
event = case arg
|
|
211
|
-
when String
|
|
212
|
-
RatatuiRuby::Event::Key.new(code: arg)
|
|
213
|
-
when Symbol
|
|
214
|
-
parts = arg.to_s.split("_")
|
|
215
|
-
code = parts.pop
|
|
216
|
-
modifiers = parts
|
|
217
|
-
RatatuiRuby::Event::Key.new(code:, modifiers:)
|
|
218
|
-
when Hash
|
|
219
|
-
RatatuiRuby::Event::Key.new(**arg)
|
|
220
|
-
when RatatuiRuby::Event::Key
|
|
221
|
-
arg
|
|
222
|
-
else
|
|
223
|
-
raise ArgumentError, "Invalid key argument: #{arg.inspect}. Expected String, Symbol, Hash, or Key event."
|
|
224
|
-
end
|
|
225
|
-
inject_event(event)
|
|
226
|
-
end
|
|
227
|
-
end
|
|
228
|
-
alias inject_key inject_keys
|
|
229
|
-
|
|
230
|
-
##
|
|
231
|
-
# Returns the cell attributes at the given coordinates.
|
|
232
|
-
#
|
|
233
|
-
# get_cell(0, 0)
|
|
234
|
-
# # => { "symbol" => "H", "fg" => :red, "bg" => nil }
|
|
235
|
-
def get_cell(x, y)
|
|
236
|
-
RatatuiRuby.get_cell_at(x, y)
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
##
|
|
240
|
-
# Asserts that the cell at the given coordinates has the expected attributes.
|
|
241
|
-
#
|
|
242
|
-
# assert_cell_style(0, 0, char: "H", fg: :red)
|
|
243
|
-
def assert_cell_style(x, y, **expected_attributes)
|
|
244
|
-
cell = get_cell(x, y)
|
|
245
|
-
expected_attributes.each do |key, value|
|
|
246
|
-
actual_value = cell.public_send(key)
|
|
247
|
-
if value.nil?
|
|
248
|
-
assert_nil actual_value, "Expected cell at (#{x}, #{y}) to have #{key}=nil, but got #{actual_value.inspect}"
|
|
249
|
-
else
|
|
250
|
-
assert_equal value, actual_value, "Expected cell at (#{x}, #{y}) to have #{key}=#{value.inspect}, but got #{actual_value.inspect}"
|
|
251
|
-
end
|
|
252
|
-
end
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
##
|
|
256
|
-
# Mock frame for unit testing views.
|
|
257
|
-
#
|
|
258
|
-
# Captures widgets passed to +render_widget+ for inspection.
|
|
259
|
-
# Does not render anything—purely captures the output.
|
|
260
|
-
#
|
|
261
|
-
# == Examples
|
|
262
|
-
#
|
|
263
|
-
# frame = MockFrame.new
|
|
264
|
-
# View::Log.new.call(state, tui, frame, area)
|
|
265
|
-
# widget = frame.rendered_widgets.first[:widget]
|
|
266
|
-
# assert_equal "Event Log", widget.block.title
|
|
267
|
-
MockFrame = Data.define(:rendered_widgets) do
|
|
268
|
-
def initialize(rendered_widgets: [])
|
|
269
|
-
super
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
def render_widget(widget, area)
|
|
273
|
-
rendered_widgets << { widget:, area: }
|
|
274
|
-
end
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
##
|
|
278
|
-
# Stub area for unit testing views.
|
|
279
|
-
#
|
|
280
|
-
# Provides the minimal interface views expect (+width+, +height+).
|
|
281
|
-
#
|
|
282
|
-
# == Examples
|
|
283
|
-
#
|
|
284
|
-
# area = StubRect.new(width: 60, height: 20)
|
|
285
|
-
StubRect = Data.define(:x, :y, :width, :height) do
|
|
286
|
-
def initialize(x: 0, y: 0, width: 80, height: 24)
|
|
287
|
-
super
|
|
288
|
-
end
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
##
|
|
292
|
-
# Asserts that the current screen content matches a stored snapshot.
|
|
293
|
-
#
|
|
294
|
-
# This method simplifies snapshot testing by automatically resolving the snapshot path
|
|
295
|
-
# relative to the test file calling this method. It assumes a "snapshots" directory
|
|
296
|
-
# exists in the same directory as the test file.
|
|
297
|
-
#
|
|
298
|
-
# # In test/test_login.rb
|
|
299
|
-
# assert_snapshot("login_screen")
|
|
300
|
-
# # Look for: test/snapshots/login_screen.txt
|
|
301
|
-
#
|
|
302
|
-
# # With normalization block
|
|
303
|
-
# assert_snapshot("clock") do |actual|
|
|
304
|
-
# actual.map { |l| l.gsub(/\d{2}:\d{2}/, "XX:XX") }
|
|
305
|
-
# end
|
|
306
|
-
#
|
|
307
|
-
# [name] String name of the snapshot (without extension).
|
|
308
|
-
# [msg] String optional failure message.
|
|
309
|
-
def assert_snapshot(name, msg = nil, &)
|
|
310
|
-
# Get the path of the test file calling this method
|
|
311
|
-
caller_path = caller_locations(1, 1).first.path
|
|
312
|
-
snapshot_dir = File.join(File.dirname(caller_path), "snapshots")
|
|
313
|
-
snapshot_path = File.join(snapshot_dir, "#{name}.txt")
|
|
314
|
-
|
|
315
|
-
assert_screen_matches(snapshot_path, msg, &)
|
|
316
|
-
end
|
|
317
|
-
|
|
318
|
-
##
|
|
319
|
-
# Asserts that the current screen content matches the expected content.
|
|
320
|
-
#
|
|
321
|
-
# Users need to verify that the entire TUI screen looks exactly as expected.
|
|
322
|
-
# Manually checking every cell or line is tedious and error-prone.
|
|
323
|
-
#
|
|
324
|
-
# This helper compares the current buffer content against an expected string (file path)
|
|
325
|
-
# or array of strings. It supports automatic snapshot creation and updating via
|
|
326
|
-
# the +UPDATE_SNAPSHOTS+ environment variable.
|
|
327
|
-
#
|
|
328
|
-
# Use it to verify complex UI states, layouts, and renderings.
|
|
329
|
-
#
|
|
330
|
-
# == Usage
|
|
331
|
-
#
|
|
332
|
-
# # Direct comparison
|
|
333
|
-
# assert_screen_matches(["Line 1", "Line 2"])
|
|
334
|
-
#
|
|
335
|
-
# # File comparison
|
|
336
|
-
# assert_screen_matches("test/snapshots/login.txt")
|
|
337
|
-
#
|
|
338
|
-
# # With normalization (e.g., masking dynamic data)
|
|
339
|
-
# assert_screen_matches("test/snapshots/dashboard.txt") do |lines|
|
|
340
|
-
# lines.map { |l| l.gsub(/User ID: \d+/, "User ID: XXX") }
|
|
341
|
-
# end
|
|
342
|
-
#
|
|
343
|
-
# [expected] String (file path) or Array<String> (content).
|
|
344
|
-
# [msg] String optional failure message.
|
|
345
|
-
def assert_screen_matches(expected, msg = nil)
|
|
346
|
-
actual_lines = buffer_content
|
|
347
|
-
|
|
348
|
-
if block_given?
|
|
349
|
-
actual_lines = yield(actual_lines)
|
|
350
|
-
end
|
|
351
|
-
|
|
352
|
-
if expected.is_a?(String)
|
|
353
|
-
# Snapshot file mode
|
|
354
|
-
snapshot_path = expected
|
|
355
|
-
update_snapshots = ENV["UPDATE_SNAPSHOTS"] == "1" || ENV["UPDATE_SNAPSHOTS"] == "true"
|
|
356
|
-
|
|
357
|
-
if !File.exist?(snapshot_path) || update_snapshots
|
|
358
|
-
FileUtils.mkdir_p(File.dirname(snapshot_path))
|
|
359
|
-
File.write(snapshot_path, "#{actual_lines.join("\n")}\n")
|
|
360
|
-
if update_snapshots
|
|
361
|
-
puts "Updated snapshot: #{snapshot_path}"
|
|
362
|
-
else
|
|
363
|
-
puts "Created snapshot: #{snapshot_path}"
|
|
364
|
-
end
|
|
365
|
-
end
|
|
366
|
-
|
|
367
|
-
expected_lines = File.readlines(snapshot_path, chomp: true)
|
|
368
|
-
else
|
|
369
|
-
# Direct comparison mode
|
|
370
|
-
expected_lines = expected
|
|
371
|
-
end
|
|
372
|
-
|
|
373
|
-
msg ||= "Screen content mismatch"
|
|
374
|
-
|
|
375
|
-
assert_equal expected_lines.size, actual_lines.size, "#{msg}: Line count mismatch"
|
|
376
|
-
|
|
377
|
-
expected_lines.each_with_index do |expected_line, i|
|
|
378
|
-
actual_line = actual_lines[i]
|
|
379
|
-
assert_equal expected_line, actual_line,
|
|
380
|
-
"#{msg}: Line #{i + 1} mismatch.\nExpected: #{expected_line.inspect}\nActual: #{actual_line.inspect}"
|
|
381
|
-
end
|
|
382
|
-
end
|
|
85
|
+
include Terminal
|
|
86
|
+
include Snapshot
|
|
87
|
+
include EventInjection
|
|
88
|
+
include StyleAssertions
|
|
89
|
+
include TestDoubles
|
|
383
90
|
end
|
|
384
91
|
end
|
data/lib/ratatui_ruby/version.rb
CHANGED
data/lib/ratatui_ruby.rb
CHANGED
|
@@ -10,6 +10,7 @@ require_relative "ratatui_ruby/schema/layout"
|
|
|
10
10
|
require_relative "ratatui_ruby/schema/block"
|
|
11
11
|
require_relative "ratatui_ruby/schema/constraint"
|
|
12
12
|
require_relative "ratatui_ruby/schema/list"
|
|
13
|
+
require_relative "ratatui_ruby/schema/list_item"
|
|
13
14
|
require_relative "ratatui_ruby/schema/style"
|
|
14
15
|
require_relative "ratatui_ruby/schema/gauge"
|
|
15
16
|
require_relative "ratatui_ruby/schema/line_gauge"
|
|
@@ -35,6 +36,9 @@ require_relative "ratatui_ruby/schema/draw"
|
|
|
35
36
|
require_relative "ratatui_ruby/event"
|
|
36
37
|
require_relative "ratatui_ruby/cell"
|
|
37
38
|
require_relative "ratatui_ruby/frame"
|
|
39
|
+
require_relative "ratatui_ruby/list_state"
|
|
40
|
+
require_relative "ratatui_ruby/table_state"
|
|
41
|
+
require_relative "ratatui_ruby/scrollbar_state"
|
|
38
42
|
|
|
39
43
|
begin
|
|
40
44
|
require "ratatui_ruby/ratatui_ruby"
|
|
@@ -52,7 +56,13 @@ end
|
|
|
52
56
|
# Use `RatatuiRuby.run` to start your application.
|
|
53
57
|
module RatatuiRuby
|
|
54
58
|
# Generic error class for RatatuiRuby.
|
|
55
|
-
class Error < StandardError
|
|
59
|
+
class Error < StandardError
|
|
60
|
+
# Raised when a terminal operation fails (e.g., I/O error, backend failure).
|
|
61
|
+
class Terminal < Error; end
|
|
62
|
+
|
|
63
|
+
# Raised when an API safety contract is violated (e.g., accessing a Frame outside its valid scope).
|
|
64
|
+
class Safety < Error; end
|
|
65
|
+
end
|
|
56
66
|
|
|
57
67
|
##
|
|
58
68
|
# Initializes the terminal for TUI mode.
|
|
@@ -155,41 +165,54 @@ module RatatuiRuby
|
|
|
155
165
|
##
|
|
156
166
|
# Checks for user input.
|
|
157
167
|
#
|
|
158
|
-
#
|
|
159
|
-
# Returns RatatuiRuby::Event::None if the queue is empty (non-blocking).
|
|
168
|
+
# Interactive apps must respond to input. Loops need to poll without burning CPU.
|
|
160
169
|
#
|
|
161
|
-
#
|
|
170
|
+
# This method checks for an event. It returns the event if one is found. It returns {RatatuiRuby::Event::None} if the timeout expires.
|
|
171
|
+
#
|
|
172
|
+
# [timeout] Float seconds to wait (default: 0.016).
|
|
173
|
+
# Pass <tt>nil</tt> to block indefinitely (wait forever).
|
|
174
|
+
# Pass <tt>0.0</tt> for a non-blocking check.
|
|
175
|
+
#
|
|
176
|
+
# === Examples
|
|
162
177
|
#
|
|
178
|
+
# # Standard loop (approx 60 FPS)
|
|
163
179
|
# event = RatatuiRuby.poll_event
|
|
164
|
-
# if event.none?
|
|
165
|
-
# puts "No input available"
|
|
166
|
-
# elsif event.key?
|
|
167
|
-
# puts "Key pressed"
|
|
168
|
-
# end
|
|
169
180
|
#
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
181
|
+
# # Block until event (pauses execution)
|
|
182
|
+
# event = RatatuiRuby.poll_event(timeout: nil)
|
|
183
|
+
#
|
|
184
|
+
# # Non-blocking check (returns immediately)
|
|
185
|
+
# event = RatatuiRuby.poll_event(timeout: 0.0)
|
|
186
|
+
#
|
|
187
|
+
def self.poll_event(timeout: 0.016)
|
|
188
|
+
raise ArgumentError, "timeout must be non-negative" if timeout && timeout < 0
|
|
189
|
+
|
|
190
|
+
raw = _poll_event(timeout)
|
|
191
|
+
return Event::None.new.freeze if raw.nil?
|
|
173
192
|
|
|
174
193
|
case raw[:type]
|
|
175
194
|
when :key
|
|
176
|
-
Event::Key.new(
|
|
195
|
+
Event::Key.new(
|
|
196
|
+
code: raw[:code],
|
|
197
|
+
modifiers: (raw[:modifiers] || []).freeze,
|
|
198
|
+
kind: raw[:kind] || :standard
|
|
199
|
+
).freeze
|
|
177
200
|
when :mouse
|
|
178
201
|
Event::Mouse.new(
|
|
179
202
|
kind: raw[:kind].to_s,
|
|
180
203
|
x: raw[:x],
|
|
181
204
|
y: raw[:y],
|
|
182
205
|
button: raw[:button].to_s,
|
|
183
|
-
modifiers: raw[:modifiers] || []
|
|
184
|
-
)
|
|
206
|
+
modifiers: (raw[:modifiers] || []).freeze
|
|
207
|
+
).freeze
|
|
185
208
|
when :resize
|
|
186
|
-
Event::Resize.new(width: raw[:width], height: raw[:height])
|
|
209
|
+
Event::Resize.new(width: raw[:width], height: raw[:height]).freeze
|
|
187
210
|
when :paste
|
|
188
|
-
Event::Paste.new(content: raw[:content])
|
|
211
|
+
Event::Paste.new(content: raw[:content]).freeze
|
|
189
212
|
when :focus_gained
|
|
190
|
-
Event::FocusGained.new
|
|
213
|
+
Event::FocusGained.new.freeze
|
|
191
214
|
when :focus_lost
|
|
192
|
-
Event::FocusLost.new
|
|
215
|
+
Event::FocusLost.new.freeze
|
|
193
216
|
else
|
|
194
217
|
# Fallback for unknown events, though ideally we cover them all
|
|
195
218
|
nil
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
class AppStatefulInteraction
|
|
5
|
+
@tables: Array[String]
|
|
6
|
+
@data: Hash[String, Array[Array[String]]]
|
|
7
|
+
@list_state: RatatuiRuby::ListState
|
|
8
|
+
@table_state: RatatuiRuby::TableState
|
|
9
|
+
@active_pane: :list | :table
|
|
10
|
+
@tui: untyped
|
|
11
|
+
@style_active: RatatuiRuby::Style
|
|
12
|
+
@style_inactive: RatatuiRuby::Style
|
|
13
|
+
@style_highlight: RatatuiRuby::Style
|
|
14
|
+
@list_area: RatatuiRuby::Rect
|
|
15
|
+
@table_area: RatatuiRuby::Rect
|
|
16
|
+
|
|
17
|
+
# @public
|
|
18
|
+
def self.new: () -> AppStatefulInteraction
|
|
19
|
+
|
|
20
|
+
# @public
|
|
21
|
+
def run: () -> void
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def render: () -> void
|
|
26
|
+
def render_list: (untyped frame, RatatuiRuby::Rect area) -> void
|
|
27
|
+
def render_table: (untyped frame, RatatuiRuby::Rect area) -> void
|
|
28
|
+
def handle_input: () -> (:quit | nil)
|
|
29
|
+
def scroll_active: (Integer delta) -> void
|
|
30
|
+
def handle_click: (Integer x, Integer y) -> void
|
|
31
|
+
def handle_list_click: (Integer mouse_y) -> void
|
|
32
|
+
def handle_table_click: (Integer mouse_y) -> void
|
|
33
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
class WidgetBlockDemo
|
|
7
|
+
@tui: RatatuiRuby::Tui
|
|
8
|
+
@hotkey_style: RatatuiRuby::Style
|
|
9
|
+
@title_configs: Array[{name: String, title: String?}]
|
|
10
|
+
@title_index: Integer
|
|
11
|
+
@titles_configs: Array[{name: String, titles: Array[RatatuiRuby::Block::title_input]}]
|
|
12
|
+
@titles_index: Integer
|
|
13
|
+
@alignment_configs: Array[{name: String, alignment: RatatuiRuby::Alignment}]
|
|
14
|
+
@alignment_index: Integer
|
|
15
|
+
@title_styles: Array[{name: String, style: RatatuiRuby::Style?}]
|
|
16
|
+
@title_style_index: Integer
|
|
17
|
+
@border_configs: Array[{name: String, borders: Array[RatatuiRuby::Block::border_option]}]
|
|
18
|
+
@border_index: Integer
|
|
19
|
+
@border_type_configs: Array[{name: String, type: RatatuiRuby::Block::border_type?, set: Hash[Symbol, String]?}]
|
|
20
|
+
@border_type_index: Integer
|
|
21
|
+
@border_styles: Array[{name: String, style: RatatuiRuby::Style?}]
|
|
22
|
+
@border_style_index: Integer
|
|
23
|
+
@base_styles: Array[{name: String, style: RatatuiRuby::Style?}]
|
|
24
|
+
@base_style_index: Integer
|
|
25
|
+
@padding_configs: Array[{name: String, padding: RatatuiRuby::Block::padding_input}]
|
|
26
|
+
@padding_index: Integer
|
|
27
|
+
|
|
28
|
+
def self.new: () -> WidgetBlockDemo
|
|
29
|
+
def run: () -> void
|
|
30
|
+
private def render: () -> void
|
|
31
|
+
private def handle_input: () -> (:quit)?
|
|
32
|
+
end
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
-
#
|
|
3
2
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
3
|
|
|
5
|
-
class
|
|
4
|
+
class WidgetTextWidth
|
|
6
5
|
# @public
|
|
7
|
-
def self.new: () ->
|
|
6
|
+
def self.new: () -> WidgetTextWidth
|
|
8
7
|
|
|
9
8
|
# @public
|
|
10
9
|
def run: () -> void
|