rooibos 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 +9 -5
- data/.builds/ruby-3.3.yml +9 -5
- data/.builds/ruby-3.4.yml +9 -5
- data/.builds/ruby-4.0.0.yml +9 -5
- data/AGENTS.md +1 -1
- data/CHANGELOG.md +46 -0
- data/README.md +2 -2
- data/README.rdoc +374 -0
- data/REUSE.toml +5 -0
- data/Rakefile +1 -1
- data/doc/best_practices/forms_and_validation.md +20 -0
- data/doc/best_practices/http_workflows.md +20 -0
- data/doc/best_practices/index.md +26 -0
- data/doc/best_practices/lists_and_tables.md +20 -0
- data/doc/best_practices/modal_dialogs.md +20 -0
- data/doc/best_practices/no_stateful_widgets.md +184 -0
- data/doc/best_practices/orchestration.md +20 -0
- data/doc/best_practices/streaming_data.md +20 -0
- data/doc/contributors/design/commands_and_outlets.md +1 -1
- data/doc/contributors/documentation_plan.md +616 -0
- data/doc/contributors/documentation_stub_audit.md +112 -0
- data/doc/contributors/documentation_style.md +275 -0
- data/doc/contributors/e2e_pty.md +168 -0
- data/doc/contributors/specs/earliest_tutorial_steps_per_story.md +70 -0
- data/doc/contributors/specs/file_browser.md +789 -0
- data/doc/contributors/specs/file_browser_stories.md +774 -0
- data/doc/contributors/specs/tutorials_to_stories.rb +167 -0
- data/doc/contributors/todo/scrollbar.md +118 -0
- data/doc/contributors/tutorial_old/01_project_setup.md +20 -0
- data/doc/contributors/tutorial_old/02_hello_world.md +24 -0
- data/doc/contributors/tutorial_old/03_adding_state.md +26 -0
- data/doc/contributors/tutorial_old/06_organizing_your_code.md +20 -0
- data/doc/contributors/tutorial_old/07_your_first_command.md +21 -0
- data/doc/contributors/tutorial_old/08_the_preview_pane.md +20 -0
- data/doc/contributors/tutorial_old/09_loading_states.md +20 -0
- data/doc/contributors/tutorial_old/10_testing_your_app.md +20 -0
- data/doc/contributors/tutorial_old/11_polish_and_refine.md +20 -0
- data/doc/contributors/tutorial_old/12_going_further.md +20 -0
- data/doc/contributors/tutorial_old/index.md +20 -0
- data/doc/essentials/commands.md +20 -0
- data/doc/essentials/index.md +31 -0
- data/doc/essentials/messages.md +21 -0
- data/doc/essentials/models.md +21 -0
- data/doc/essentials/shortcuts.md +19 -0
- data/doc/essentials/the_elm_architecture.md +24 -0
- data/doc/essentials/the_runtime.md +21 -0
- data/doc/essentials/update_functions.md +20 -0
- data/doc/essentials/views.md +22 -0
- data/doc/getting_started/for_go_developers.md +16 -0
- data/doc/getting_started/for_python_developers.md +16 -0
- data/doc/getting_started/for_react_developers.md +17 -0
- data/doc/getting_started/index.md +52 -0
- data/doc/getting_started/install.md +20 -0
- data/doc/getting_started/quickstart.md +9 -45
- data/doc/getting_started/ruby_primer.md +19 -0
- data/doc/getting_started/why_rooibos.md +20 -0
- data/doc/index.md +79 -11
- data/doc/scaling_up/async_patterns.md +20 -0
- data/doc/scaling_up/command_composition.md +20 -0
- data/doc/scaling_up/custom_commands.md +21 -0
- data/doc/scaling_up/fractal_architecture.md +20 -0
- data/doc/scaling_up/index.md +30 -0
- data/doc/scaling_up/message_routing.md +20 -0
- data/doc/scaling_up/ractor_safety.md +20 -0
- data/doc/scaling_up/testing.md +21 -0
- data/doc/troubleshooting/common_errors.md +20 -0
- data/doc/troubleshooting/debugging.md +21 -0
- data/doc/troubleshooting/index.md +23 -0
- data/doc/troubleshooting/performance.md +20 -0
- data/doc/tutorial/01_project_setup.md +44 -0
- data/doc/tutorial/02_hello_world.md +45 -0
- data/doc/tutorial/03_static_file_list.md +44 -0
- data/doc/tutorial/04_arrow_navigation.md +47 -0
- data/doc/tutorial/05_real_files.md +45 -0
- data/doc/tutorial/06_safe_refactoring.md +21 -0
- data/doc/tutorial/07_red_first_tdd.md +26 -0
- data/doc/tutorial/08_file_metadata.md +42 -0
- data/doc/tutorial/09_text_preview.md +44 -0
- data/doc/tutorial/10_directory_tree.md +42 -0
- data/doc/tutorial/11_pane_focus.md +40 -0
- data/doc/tutorial/12_sorting.md +41 -0
- data/doc/tutorial/13_filtering.md +43 -0
- data/doc/tutorial/14_toggle_hidden.md +41 -0
- data/doc/tutorial/15_text_input_widget.md +43 -0
- data/doc/tutorial/16_rename_files.md +42 -0
- data/doc/tutorial/17_confirmation_dialogs.md +43 -0
- data/doc/tutorial/18_progress_indicators.md +43 -0
- data/doc/tutorial/19_atomic_operations.md +42 -0
- data/doc/tutorial/20_external_editor.md +42 -0
- data/doc/tutorial/21_modal_overlays.md +41 -0
- data/doc/tutorial/22_error_handling.md +43 -0
- data/doc/tutorial/23_terminal_capabilities.md +53 -0
- data/doc/tutorial/24_mouse_events.md +43 -0
- data/doc/tutorial/25_resize_events.md +43 -0
- data/doc/tutorial/26_loading_states.md +42 -0
- data/doc/tutorial/27_performance.md +43 -0
- data/doc/tutorial/28_color_schemes.md +47 -0
- data/doc/tutorial/29_configuration.md +124 -0
- data/doc/tutorial/30_going_further.md +17 -0
- data/doc/tutorial/index.md +17 -0
- data/examples/app_file_browser/app.rb +40 -0
- data/examples/app_fractal_dashboard/dashboard/update_manual.rb +7 -7
- data/examples/app_fractal_dashboard/fragments/custom_shell_input.rb +5 -5
- data/examples/app_fractal_dashboard/fragments/custom_shell_modal.rb +1 -1
- data/examples/app_fractal_dashboard/fragments/disk_usage.rb +2 -2
- data/examples/app_fractal_dashboard/fragments/network_panel.rb +4 -4
- data/examples/app_fractal_dashboard/fragments/ping.rb +2 -2
- data/examples/app_fractal_dashboard/fragments/stats_panel.rb +4 -4
- data/examples/app_fractal_dashboard/fragments/system_info.rb +2 -2
- data/examples/app_fractal_dashboard/fragments/uptime.rb +2 -2
- data/examples/verify_website_first_app/app.rb +85 -0
- data/examples/verify_website_hello_mvu/app.rb +31 -0
- data/examples/widget_command_system/app.rb +15 -13
- data/exe/rooibos +10 -0
- data/generate_tutorial_stubs.rb +126 -0
- data/lib/rooibos/cli/commands/new.rb +373 -0
- data/lib/rooibos/cli/commands/run.rb +98 -0
- data/lib/rooibos/cli.rb +78 -0
- data/lib/rooibos/command/all.rb +25 -20
- data/lib/rooibos/command/batch.rb +26 -25
- data/lib/rooibos/command/custom.rb +84 -1
- data/lib/rooibos/command/http.rb +59 -55
- data/lib/rooibos/command/lifecycle.rb +5 -5
- data/lib/rooibos/command/open.rb +86 -0
- data/lib/rooibos/command/outlet.rb +105 -3
- data/lib/rooibos/command/wait.rb +5 -5
- data/lib/rooibos/command.rb +57 -74
- data/lib/rooibos/message/batch.rb +39 -0
- data/lib/rooibos/message/canceled.rb +51 -0
- data/lib/rooibos/message/error.rb +48 -0
- data/lib/rooibos/message/open.rb +30 -0
- data/lib/rooibos/message.rb +84 -4
- data/lib/rooibos/router.rb +11 -14
- data/lib/rooibos/runtime.rb +40 -43
- data/lib/rooibos/shortcuts.rb +47 -0
- data/lib/rooibos/test_helper.rb +71 -6
- data/lib/rooibos/version.rb +1 -1
- data/lib/rooibos/welcome.rb +237 -0
- data/lib/rooibos.rb +4 -3
- data/mise.toml +1 -1
- data/rbs_collection.lock.yaml +2 -2
- data/sig/concurrent.rbs +3 -0
- data/sig/gem.rbs +20 -0
- data/sig/rooibos/cli.rbs +42 -0
- data/sig/rooibos/command.rbs +48 -0
- data/sig/rooibos/message.rbs +60 -0
- data/sig/rooibos/shortcuts.rbs +14 -0
- data/sig/rooibos/test_helper.rbs +6 -2
- data/sig/rooibos/welcome.rbs +75 -0
- data/tasks/install.rake +29 -0
- data/tasks/resources/build.yml.erb +2 -0
- metadata +272 -38
- data/doc/concepts/application_architecture.md +0 -197
- data/doc/concepts/application_testing.md +0 -49
- data/doc/concepts/async_work.md +0 -164
- data/doc/concepts/commands.md +0 -530
- data/doc/concepts/message_processing.md +0 -51
- data/doc/contributors/WIP/decomposition_strategies_analysis.md +0 -258
- data/doc/contributors/WIP/implementation_plan.md +0 -409
- data/doc/contributors/WIP/init_callable_proposal.md +0 -344
- data/doc/contributors/WIP/runtime_refactoring_status.md +0 -47
- data/doc/contributors/WIP/task.md +0 -36
- data/doc/contributors/WIP/v0.4.0_todo.md +0 -468
- data/doc/contributors/kit-no-outlet.md +0 -238
- data/doc/contributors/priorities.md +0 -38
- data/doc/images/.gitkeep +0 -0
- data/exe/.gitkeep +0 -0
- /data/doc/contributors/{WIP → design}/mvu_tea_implementations_research.md +0 -0
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rooibos
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kerrick Long
|
|
@@ -15,28 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version:
|
|
18
|
+
version: '1.2'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version:
|
|
26
|
-
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: ostruct
|
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
|
29
|
-
requirements:
|
|
30
|
-
- - "~>"
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '0.6'
|
|
33
|
-
type: :runtime
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - "~>"
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '0.6'
|
|
25
|
+
version: '1.2'
|
|
40
26
|
- !ruby/object:Gem::Dependency
|
|
41
27
|
name: concurrent-ruby
|
|
42
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -93,11 +79,168 @@ dependencies:
|
|
|
93
79
|
- - "~>"
|
|
94
80
|
- !ruby/object:Gem::Version
|
|
95
81
|
version: '3.5'
|
|
96
|
-
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: minitest-mock
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '5.27'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '5.27'
|
|
96
|
+
description: "== Confidently Build Terminal Apps\n\nRooibos[https://rooibos.run] helps
|
|
97
|
+
you build interactive terminal applications.\nKeep your code understandable and
|
|
98
|
+
testable as it scales. Rooibos handles\nkeyboard, mouse, and async work so you can
|
|
99
|
+
focus on behavior and user experience.\n\n gem install rooibos\n\n<i>Currently
|
|
100
|
+
in beta. APIs may change before 1.0.</i>\n\n=== Get Started in Seconds\n\n rooibos
|
|
101
|
+
new my_app\n cd my_app\n rooibos run\n\nThat's it. You have a working app with
|
|
102
|
+
keyboard navigation, mouse support,\nand clickable buttons. Open <tt>lib/my_app.rb</tt>
|
|
103
|
+
to make it your own.\n\n\n---\n\n=== The Pattern\n\n\\Rooibos uses Model-View-Update,
|
|
104
|
+
the architecture behind\nElm[https://guide.elm-lang.org/architecture/],\nRedux[https://redux.js.org/],
|
|
105
|
+
and {Bubble\nTea}[https://github.com/charmbracelet/bubbletea].\nState lives in one
|
|
106
|
+
place. Updates flow in one direction. The runtime handles\nrendering and runs background
|
|
107
|
+
work for you.\n\n---\n\n=== Hello, MVU\n\nThe simplest \\Rooibos app. Press any
|
|
108
|
+
key to increment the counter. Press\n<tt>Ctrl</tt>+<tt>C</tt> to quit.\n\n require
|
|
109
|
+
\"rooibos\"\n\n module Counter\n # Init: How do you create the initial model?\n
|
|
110
|
+
\ Init = -> { 0 }\n \n # View: What does the user see?\n View = -> (model,
|
|
111
|
+
tui) { tui.paragraph(text: <<~END) }\n Current count: #{model}.\n Press
|
|
112
|
+
any key to increment.\n Press Ctrl+C to quit.\n END\n \n # Update: What
|
|
113
|
+
happens when things change?\n Update = -> (message, model) {\n if message.ctrl_c?\n
|
|
114
|
+
\ Rooibos::Command.exit\n elsif message.key?\n model + 1\n end\n
|
|
115
|
+
\ }\n end\n\n Rooibos.run(Counter)\n\nThat's the whole pattern: Model holds
|
|
116
|
+
state, Init creates it, View renders it,\nand Update changes it. The runtime handles
|
|
117
|
+
everything else.\n\n\n---\n\n=== Your First Real Application\n\nA file browser in
|
|
118
|
+
sixty lines. It opens files, navigates directories, handles\nerrors, styles directories
|
|
119
|
+
and hidden files differently, and supports vim-style\nkeyboard shortcuts. If you
|
|
120
|
+
can do this much with this little code, imagine how\neasy _your_ app will be to
|
|
121
|
+
build.\n\n require \"rooibos\"\n \n module FileBrowser\n # Model: What state
|
|
122
|
+
does your app need?\n Model = Data.define(:path, :entries, :selected, :error)\n
|
|
123
|
+
\ \n Init = -> {\n path = Dir.pwd\n entries = Entries[path]\n Ractor.make_shareable(
|
|
124
|
+
# Ensures thread safety\n Model.new(path:, entries:, selected: entries.first,
|
|
125
|
+
error: nil))\n }\n \n View = -> (model, tui) {\n tui.block(\n titles:
|
|
126
|
+
[model.error || model.path,\n { content: KEYS, position: :bottom,
|
|
127
|
+
alignment: :right}],\n borders: [:all],\n border_style: if model.error
|
|
128
|
+
then tui.style(fg: :red) else nil end,\n children: [tui.list(items: model.entries.map(&ListItem[model,
|
|
129
|
+
tui]),\n selected_index: model.entries.index(model.selected),\n
|
|
130
|
+
\ highlight_symbol: \"\",\n highlight_style:
|
|
131
|
+
tui.style(modifiers: [:reversed]))]\n )\n }\n \n Update = -> (message,
|
|
132
|
+
model) {\n return model.with(error: ERROR) if message.error?\n model =
|
|
133
|
+
model.with(error: nil) if model.error && message.key?\n \n if message.ctrl_c?
|
|
134
|
+
|| message.q? then Rooibos::Command.exit\n elsif message.home? || message.g?
|
|
135
|
+
then model.with(selected: model.entries.first)\n elsif message.end? || message.G?
|
|
136
|
+
then model.with(selected: model.entries.last)\n elsif message.up_arrow? ||
|
|
137
|
+
message.k? then Select[:-, model]\n elsif message.down_arrow? || message.j?
|
|
138
|
+
then Select[:+, model]\n elsif message.enter? then Open[model]\n elsif
|
|
139
|
+
message.escape? then Navigate[File.dirname(model.path), model]\n end\n }\n
|
|
140
|
+
\ \n private # Lines below this are implementation details\n \n KEYS = \"↑/↓/Home/End:
|
|
141
|
+
Select | Enter: Open | Esc: Navigate Up | q: Quit\"\n ERROR = \"Sorry, opening
|
|
142
|
+
the selected file failed.\"\n \n ListItem = -> (model, tui) { -> (name) {\n
|
|
143
|
+
\ modifiers = name.start_with?(\".\") ? [:dim] : []\n fg = :blue
|
|
144
|
+
if name.end_with?(\"/\")\n tui.list_item(content: name, style: tui.style(fg:,
|
|
145
|
+
modifiers:))\n } }\n \n Select = -> (operator, model) {\n new_index
|
|
146
|
+
= model.entries.index(model.selected).public_send(operator, 1)\n model.with(selected:
|
|
147
|
+
model.entries[new_index.clamp(0, model.entries.length - 1)])\n }\n \n Open
|
|
148
|
+
= -> (model) {\n full = File.join(model.path, model.selected.delete_suffix(\"/\"))\n
|
|
149
|
+
\ model.selected.end_with?(\"/\") ? Navigate[full, model] : Rooibos::Command.open(full)\n
|
|
150
|
+
\ }\n \n Navigate = -> (path, model) {\n entries = Entries[path]\n model.with(path:,
|
|
151
|
+
entries:, selected: entries.first, error: nil)\n }\n \n Entries = -> (path)
|
|
152
|
+
{\n Dir.children(path).map { |name|\n File.directory?(File.join(path,
|
|
153
|
+
name)) ? \"#{name}/\" : name\n }.sort_by { |name| [name.end_with?(\"/\") ?
|
|
154
|
+
0 : 1, name.downcase] }\n }\n end\n\n Rooibos.run(FileBrowser)\n\n\n---\n\n===
|
|
155
|
+
Batteries Included\n\n==== Commands\n\nApplications fetch data, run shell commands,
|
|
156
|
+
and set timers. \\Rooibos Commands\nrun off the main thread and send results back
|
|
157
|
+
as messages.\n\n<b>HTTP requests:</b>\n\n Update = -> (message, model) {\n case
|
|
158
|
+
message\n in :fetch_users\n [model.with(loading: true), Rooibos::Command.http(:get,
|
|
159
|
+
\"/api/users\", :got_users)]\n in { type: :http, envelope: :got_users, status:
|
|
160
|
+
200, body: }\n model.with(loading: false, users: JSON.parse(body))\n in
|
|
161
|
+
{ type: :http, envelope: :got_users, status: }\n model.with(error: \"HTTP #{status}\")\n
|
|
162
|
+
\ end\n }\n\n<b>Shell commands:</b>\n\n Update = -> (message, model) {\n case
|
|
163
|
+
message\n in :list_files\n Rooibos::Command.system(\"ls -la\", :listed_files)\n
|
|
164
|
+
\ in { type: :system, envelope: :listed_files, stdout:, status: 0 }\n model.with(files:
|
|
165
|
+
stdout.lines.map(&:chomp))\n in { type: :system, envelope: :listed_files, stderr:,
|
|
166
|
+
status: }\n model.with(error: stderr)\n end\n }\n\n<b>Timers:</b>\n\n Update
|
|
167
|
+
= -> (message, model) {\n case message\n in { type: :timer, envelope: :tick,
|
|
168
|
+
elapsed: }\n [model.with(frame: model.frame + 1), Rooibos::Command.wait(1.0
|
|
169
|
+
/ 24, :tick)]\n end\n }\n\n<b>And more!</b> \\Rooibos includes <tt>all</tt>,
|
|
170
|
+
<tt>batch</tt>, <tt>cancel</tt>,\n<tt>custom</tt>, <tt>exit</tt>, <tt>http</tt>,
|
|
171
|
+
<tt>map</tt>, <tt>open</tt>,\n<tt>system</tt>, <tt>tick</tt>, and <tt>wait</tt>
|
|
172
|
+
commands. You can also define\nyour own custom commands for complex orchestration.
|
|
173
|
+
\n\nEvery command produces a message, and Update handles it the same way.\n\n====
|
|
174
|
+
Testing\n\n\\Rooibos makes TUIs so easy to test, you'll save more time by writing
|
|
175
|
+
tests than\nby not testing.\n\n<b>Unit test Update, View, and Init.</b> No terminal
|
|
176
|
+
needed. Test helpers included.\n\n def test_moves_selection_down_with_j\n model
|
|
177
|
+
= Ractor.make_shareable(FileBrowser::Model.new(\n path: \"/\", entries: %w[bin
|
|
178
|
+
exe lib], selected: \"bin\", error: nil))\n message = RatatuiRuby::Event::Key.new(code:
|
|
179
|
+
\"j\")\n\n result = FileBrowser::Update.call(message, model)\n\n assert_equal
|
|
180
|
+
\"exe\", result.selected\n end\n\n<b>Style assertions.</b> Draw to a headless terminal,
|
|
181
|
+
verify colors and modifiers.\n\n def test_directories_are_blue\n with_test_terminal(60,
|
|
182
|
+
10) do\n model = Ractor.make_shareable(FileBrowser::Model.new(\n path:
|
|
183
|
+
\"/\", entries: %w[file.txt subdir/], selected: \"file.txt\", error: nil))\n widget
|
|
184
|
+
= FileBrowser::View.call(model, RatatuiRuby::TUI.new)\n\n RatatuiRuby.draw
|
|
185
|
+
{ |frame| frame.render_widget(widget, frame.area) }\n\n assert_blue(1, 2) #
|
|
186
|
+
\"subdir/\" at column 1, row 2\n end\n end\n\n<b>System tests.</b> Inject events,
|
|
187
|
+
run the full app, snapshot the result.\n\n def test_selection_moves_down\n with_test_terminal(120,
|
|
188
|
+
30) do\n Dir.mktmpdir do |dir|\n FileUtils.touch(File.join(dir, \"a\"))\n
|
|
189
|
+
\ FileUtils.touch(File.join(dir, \"b\"))\n FileUtils.touch(File.join(dir,
|
|
190
|
+
\"c\"))\n\n inject_key(:down)\n inject_key(:ctrl_c)\n\n # Tests
|
|
191
|
+
use explicit params to inject deterministic initial state.\n Rooibos.run(\n
|
|
192
|
+
\ model: Ractor.make_shareable(FileBrowser::Model.new(\n path:
|
|
193
|
+
dir, entries: %w[a b c], selected: \"a\", error: nil)),\n view: FileBrowser::View,\n
|
|
194
|
+
\ update: FileBrowser::Update\n )\n\n assert_snapshots(\"selection_moved_down\")
|
|
195
|
+
do |lines|\n title = \"┌/tmp/test#{'─' * 107}┐\"\n lines.map do
|
|
196
|
+
|l|\n l.gsub(/┌#{Regexp.escape(dir)}[^┐]*┐/, title)\n end\n
|
|
197
|
+
\ end\n end\n end\n end\n\nSnapshots record both plain text and ANSI
|
|
198
|
+
colors. Normalization blocks mask\ndynamic content (timestamps, temp paths) for
|
|
199
|
+
cross-platform reproducibility.\nRun <tt>UPDATE_SNAPSHOTS=1 rake test</tt> to regenerate
|
|
200
|
+
baselines.\n\n==== Scale Up\n\nLarge applications decompose into fragments. Each
|
|
201
|
+
fragment has its own Model,\nView, Update, and Init. Parents compose children. The
|
|
202
|
+
pattern scales.\n\nThe Router DSL eliminates boilerplate:\n\n module Dashboard\n
|
|
203
|
+
\ include Rooibos::Router\n\n route :stats, to: StatsPanel\n route :network,
|
|
204
|
+
to: NetworkPanel\n\n keymap do\n key :ctrl_c, -> { Rooibos::Command.exit
|
|
205
|
+
}\n only when: -> (model) { !model.modal_open } do\n key :q, -> { Rooibos::Command.exit
|
|
206
|
+
}\n key :s, -> { StatsPanel.fetch_command }\n key :p, -> { NetworkPanel.ping_command
|
|
207
|
+
}\n end\n end\n\n Update = from_router\n\n # ... Model, Init, View
|
|
208
|
+
below\n end\n\nDeclare routes and keymaps. The router generates Update for you.
|
|
209
|
+
Use guards to\nignore keys when needed.\n\n==== CLI\n\nThe <tt>rooibos</tt> command
|
|
210
|
+
scaffolds projects and runs applications.\n\n rooibos new my_app # Generate
|
|
211
|
+
project structure\n rooibos run # Run the app in current directory\n\nGenerated
|
|
212
|
+
apps include tests, type signatures, and a working welcome\nscreen with keyboard
|
|
213
|
+
and mouse support.\n\n\n---\n\n=== The Ecosystem\n\n\\Rooibos builds on RatatuiRuby[https://www.ratatui-ruby.dev],
|
|
214
|
+
a Rubygem built on\nRatatui[https://ratatui.rs]. You get native performance with
|
|
215
|
+
the joy of Ruby.\n\\Rooibos is one way to manage state and composition. Kit is another.\n\n====
|
|
216
|
+
Rooibos[https://git.sr.ht/~kerrick/rooibos]\n\nModel-View-Update architecture. Inspired
|
|
217
|
+
by Elm, Bubble Tea, and React +\nRedux. Your UI is a pure function of state.\n\n-
|
|
218
|
+
Functional programming with MVU\n- Commands work off the main thread\n- Messages,
|
|
219
|
+
not callbacks, drive updates\n\n==== {Kit}[https://sr.ht/~kerrick/ratatui_ruby/#chapter-3-the-object-path--kit]
|
|
220
|
+
(Coming Soon)\n\nComponent-based architecture. Encapsulate state, input handling,
|
|
221
|
+
and\nrendering in reusable pieces.\n\n- OOP with stateful components\n- Separate
|
|
222
|
+
UI state from domain logic\n- Built-in focus management & click handling\n\nBoth
|
|
223
|
+
use the same widget library and rendering engine. Pick the paradigm\nthat fits your
|
|
224
|
+
brain.\n\n\n---\n\n=== Links\n\n[Get Started]\n {Getting Started}[https://git.sr.ht/~kerrick/rooibos/tree/trunk/item/doc/getting_started/index.md],\n
|
|
225
|
+
\ {Tutorial}[https://git.sr.ht/~kerrick/rooibos/tree/trunk/item/doc/tutorial/index.md],\n
|
|
226
|
+
\ {Examples}[https://git.sr.ht/~kerrick/rooibos/tree/trunk/item/examples]\n\n[Coming
|
|
227
|
+
From...]\n {React/Redux}[https://git.sr.ht/~kerrick/rooibos/tree/trunk/item/doc/getting_started/for_react_developers.md],\n
|
|
228
|
+
\ {BubbleTea}[https://git.sr.ht/~kerrick/rooibos/tree/trunk/item/doc/getting_started/for_go_developers.md],\n
|
|
229
|
+
\ {Textual}[https://git.sr.ht/~kerrick/rooibos/tree/trunk/item/doc/getting_started/for_python_developers.md]\n\n[Learn
|
|
230
|
+
More]\n {Essentials}[https://git.sr.ht/~kerrick/rooibos/tree/trunk/item/doc/essentials/index.md],\n
|
|
231
|
+
\ {Scaling Up}[https://git.sr.ht/~kerrick/rooibos/tree/trunk/item/doc/scaling_up/index.md],\n
|
|
232
|
+
\ {Best Practices}[https://git.sr.ht/~kerrick/rooibos/tree/trunk/item/doc/best_practices/index.md],\n
|
|
233
|
+
\ {Troubleshooting}[https://git.sr.ht/~kerrick/rooibos/tree/trunk/item/doc/troubleshooting/index.md]\n\n[Community]\n
|
|
234
|
+
\ {Discuss}[https://lists.sr.ht/~kerrick/ratatui_ruby-discuss],\n {Announcements}[https://lists.sr.ht/~kerrick/ratatui_ruby-announce],\n
|
|
235
|
+
\ {Bug Tracker}[https://todo.sr.ht/~kerrick/ratatui_ruby],\n {Contribution Guide}[https://man.sr.ht/~kerrick/ratatui_ruby/contributing.md],\n
|
|
236
|
+
\ {Code of Conduct}[https://man.sr.ht/~kerrick/ratatui_ruby/code_of_conduct.md]\n\n\n---\n\n[Website]
|
|
237
|
+
https://rooibos.run\n[Source] https://git.sr.ht/~kerrick/rooibos\n[RubyGems] https://rubygems.org/gems/rooibos\n\n©
|
|
238
|
+
2026 Kerrick Long · Library: LGPL-3.0-or-later · Website: CC-BY-NC-ND-4.0 · Snippets:
|
|
239
|
+
MIT-0\n"
|
|
97
240
|
email:
|
|
98
241
|
- me@kerricklong.com
|
|
99
242
|
executables:
|
|
100
|
-
-
|
|
243
|
+
- rooibos
|
|
101
244
|
extensions: []
|
|
102
245
|
extra_rdoc_files: []
|
|
103
246
|
files:
|
|
@@ -117,30 +260,105 @@ files:
|
|
|
117
260
|
- LICENSES/MIT-0.txt
|
|
118
261
|
- LICENSES/MIT.txt
|
|
119
262
|
- README.md
|
|
263
|
+
- README.rdoc
|
|
120
264
|
- REUSE.toml
|
|
121
265
|
- Rakefile
|
|
122
266
|
- Steepfile
|
|
123
|
-
- doc/
|
|
124
|
-
- doc/
|
|
125
|
-
- doc/
|
|
126
|
-
- doc/
|
|
127
|
-
- doc/
|
|
128
|
-
- doc/
|
|
129
|
-
- doc/
|
|
130
|
-
- doc/
|
|
131
|
-
- doc/contributors/WIP/mvu_tea_implementations_research.md
|
|
132
|
-
- doc/contributors/WIP/runtime_refactoring_status.md
|
|
133
|
-
- doc/contributors/WIP/task.md
|
|
134
|
-
- doc/contributors/WIP/v0.4.0_todo.md
|
|
267
|
+
- doc/best_practices/forms_and_validation.md
|
|
268
|
+
- doc/best_practices/http_workflows.md
|
|
269
|
+
- doc/best_practices/index.md
|
|
270
|
+
- doc/best_practices/lists_and_tables.md
|
|
271
|
+
- doc/best_practices/modal_dialogs.md
|
|
272
|
+
- doc/best_practices/no_stateful_widgets.md
|
|
273
|
+
- doc/best_practices/orchestration.md
|
|
274
|
+
- doc/best_practices/streaming_data.md
|
|
135
275
|
- doc/contributors/design/commands_and_outlets.md
|
|
136
|
-
- doc/contributors/
|
|
137
|
-
- doc/contributors/
|
|
276
|
+
- doc/contributors/design/mvu_tea_implementations_research.md
|
|
277
|
+
- doc/contributors/documentation_plan.md
|
|
278
|
+
- doc/contributors/documentation_stub_audit.md
|
|
279
|
+
- doc/contributors/documentation_style.md
|
|
280
|
+
- doc/contributors/e2e_pty.md
|
|
281
|
+
- doc/contributors/specs/earliest_tutorial_steps_per_story.md
|
|
282
|
+
- doc/contributors/specs/file_browser.md
|
|
283
|
+
- doc/contributors/specs/file_browser_stories.md
|
|
284
|
+
- doc/contributors/specs/tutorials_to_stories.rb
|
|
285
|
+
- doc/contributors/todo/scrollbar.md
|
|
286
|
+
- doc/contributors/tutorial_old/01_project_setup.md
|
|
287
|
+
- doc/contributors/tutorial_old/02_hello_world.md
|
|
288
|
+
- doc/contributors/tutorial_old/03_adding_state.md
|
|
289
|
+
- doc/contributors/tutorial_old/06_organizing_your_code.md
|
|
290
|
+
- doc/contributors/tutorial_old/07_your_first_command.md
|
|
291
|
+
- doc/contributors/tutorial_old/08_the_preview_pane.md
|
|
292
|
+
- doc/contributors/tutorial_old/09_loading_states.md
|
|
293
|
+
- doc/contributors/tutorial_old/10_testing_your_app.md
|
|
294
|
+
- doc/contributors/tutorial_old/11_polish_and_refine.md
|
|
295
|
+
- doc/contributors/tutorial_old/12_going_further.md
|
|
296
|
+
- doc/contributors/tutorial_old/index.md
|
|
138
297
|
- doc/custom.css
|
|
298
|
+
- doc/essentials/commands.md
|
|
299
|
+
- doc/essentials/index.md
|
|
300
|
+
- doc/essentials/messages.md
|
|
301
|
+
- doc/essentials/models.md
|
|
302
|
+
- doc/essentials/shortcuts.md
|
|
303
|
+
- doc/essentials/the_elm_architecture.md
|
|
304
|
+
- doc/essentials/the_runtime.md
|
|
305
|
+
- doc/essentials/update_functions.md
|
|
306
|
+
- doc/essentials/views.md
|
|
307
|
+
- doc/getting_started/for_go_developers.md
|
|
308
|
+
- doc/getting_started/for_python_developers.md
|
|
309
|
+
- doc/getting_started/for_react_developers.md
|
|
310
|
+
- doc/getting_started/index.md
|
|
311
|
+
- doc/getting_started/install.md
|
|
139
312
|
- doc/getting_started/quickstart.md
|
|
140
|
-
- doc/
|
|
313
|
+
- doc/getting_started/ruby_primer.md
|
|
314
|
+
- doc/getting_started/why_rooibos.md
|
|
141
315
|
- doc/images/verify_readme_usage.png
|
|
142
316
|
- doc/images/widget_cmd_exec.png
|
|
143
317
|
- doc/index.md
|
|
318
|
+
- doc/scaling_up/async_patterns.md
|
|
319
|
+
- doc/scaling_up/command_composition.md
|
|
320
|
+
- doc/scaling_up/custom_commands.md
|
|
321
|
+
- doc/scaling_up/fractal_architecture.md
|
|
322
|
+
- doc/scaling_up/index.md
|
|
323
|
+
- doc/scaling_up/message_routing.md
|
|
324
|
+
- doc/scaling_up/ractor_safety.md
|
|
325
|
+
- doc/scaling_up/testing.md
|
|
326
|
+
- doc/troubleshooting/common_errors.md
|
|
327
|
+
- doc/troubleshooting/debugging.md
|
|
328
|
+
- doc/troubleshooting/index.md
|
|
329
|
+
- doc/troubleshooting/performance.md
|
|
330
|
+
- doc/tutorial/01_project_setup.md
|
|
331
|
+
- doc/tutorial/02_hello_world.md
|
|
332
|
+
- doc/tutorial/03_static_file_list.md
|
|
333
|
+
- doc/tutorial/04_arrow_navigation.md
|
|
334
|
+
- doc/tutorial/05_real_files.md
|
|
335
|
+
- doc/tutorial/06_safe_refactoring.md
|
|
336
|
+
- doc/tutorial/07_red_first_tdd.md
|
|
337
|
+
- doc/tutorial/08_file_metadata.md
|
|
338
|
+
- doc/tutorial/09_text_preview.md
|
|
339
|
+
- doc/tutorial/10_directory_tree.md
|
|
340
|
+
- doc/tutorial/11_pane_focus.md
|
|
341
|
+
- doc/tutorial/12_sorting.md
|
|
342
|
+
- doc/tutorial/13_filtering.md
|
|
343
|
+
- doc/tutorial/14_toggle_hidden.md
|
|
344
|
+
- doc/tutorial/15_text_input_widget.md
|
|
345
|
+
- doc/tutorial/16_rename_files.md
|
|
346
|
+
- doc/tutorial/17_confirmation_dialogs.md
|
|
347
|
+
- doc/tutorial/18_progress_indicators.md
|
|
348
|
+
- doc/tutorial/19_atomic_operations.md
|
|
349
|
+
- doc/tutorial/20_external_editor.md
|
|
350
|
+
- doc/tutorial/21_modal_overlays.md
|
|
351
|
+
- doc/tutorial/22_error_handling.md
|
|
352
|
+
- doc/tutorial/23_terminal_capabilities.md
|
|
353
|
+
- doc/tutorial/24_mouse_events.md
|
|
354
|
+
- doc/tutorial/25_resize_events.md
|
|
355
|
+
- doc/tutorial/26_loading_states.md
|
|
356
|
+
- doc/tutorial/27_performance.md
|
|
357
|
+
- doc/tutorial/28_color_schemes.md
|
|
358
|
+
- doc/tutorial/29_configuration.md
|
|
359
|
+
- doc/tutorial/30_going_further.md
|
|
360
|
+
- doc/tutorial/index.md
|
|
361
|
+
- examples/app_file_browser/app.rb
|
|
144
362
|
- examples/app_fractal_dashboard/README.md
|
|
145
363
|
- examples/app_fractal_dashboard/app.rb
|
|
146
364
|
- examples/app_fractal_dashboard/dashboard/base.rb
|
|
@@ -158,22 +376,33 @@ files:
|
|
|
158
376
|
- examples/app_fractal_dashboard/fragments/uptime.rb
|
|
159
377
|
- examples/verify_readme_usage/README.md
|
|
160
378
|
- examples/verify_readme_usage/app.rb
|
|
379
|
+
- examples/verify_website_first_app/app.rb
|
|
380
|
+
- examples/verify_website_hello_mvu/app.rb
|
|
161
381
|
- examples/widget_command_system/README.md
|
|
162
382
|
- examples/widget_command_system/app.rb
|
|
163
|
-
- exe
|
|
383
|
+
- exe/rooibos
|
|
384
|
+
- generate_tutorial_stubs.rb
|
|
164
385
|
- lib/rooibos.rb
|
|
386
|
+
- lib/rooibos/cli.rb
|
|
387
|
+
- lib/rooibos/cli/commands/new.rb
|
|
388
|
+
- lib/rooibos/cli/commands/run.rb
|
|
165
389
|
- lib/rooibos/command.rb
|
|
166
390
|
- lib/rooibos/command/all.rb
|
|
167
391
|
- lib/rooibos/command/batch.rb
|
|
168
392
|
- lib/rooibos/command/custom.rb
|
|
169
393
|
- lib/rooibos/command/http.rb
|
|
170
394
|
- lib/rooibos/command/lifecycle.rb
|
|
395
|
+
- lib/rooibos/command/open.rb
|
|
171
396
|
- lib/rooibos/command/outlet.rb
|
|
172
397
|
- lib/rooibos/command/wait.rb
|
|
173
398
|
- lib/rooibos/error.rb
|
|
174
399
|
- lib/rooibos/message.rb
|
|
175
400
|
- lib/rooibos/message/all.rb
|
|
401
|
+
- lib/rooibos/message/batch.rb
|
|
402
|
+
- lib/rooibos/message/canceled.rb
|
|
403
|
+
- lib/rooibos/message/error.rb
|
|
176
404
|
- lib/rooibos/message/http_response.rb
|
|
405
|
+
- lib/rooibos/message/open.rb
|
|
177
406
|
- lib/rooibos/message/system/batch.rb
|
|
178
407
|
- lib/rooibos/message/system/stream.rb
|
|
179
408
|
- lib/rooibos/message/timer.rb
|
|
@@ -182,14 +411,17 @@ files:
|
|
|
182
411
|
- lib/rooibos/shortcuts.rb
|
|
183
412
|
- lib/rooibos/test_helper.rb
|
|
184
413
|
- lib/rooibos/version.rb
|
|
414
|
+
- lib/rooibos/welcome.rb
|
|
185
415
|
- mise.toml
|
|
186
416
|
- rbs_collection.lock.yaml
|
|
187
417
|
- rbs_collection.yaml
|
|
188
418
|
- sig/concurrent.rbs
|
|
189
419
|
- sig/examples/verify_readme_usage/app.rbs
|
|
190
420
|
- sig/examples/widget_command_system/app.rbs
|
|
421
|
+
- sig/gem.rbs
|
|
191
422
|
- sig/open3.rbs
|
|
192
423
|
- sig/rooibos.rbs
|
|
424
|
+
- sig/rooibos/cli.rbs
|
|
193
425
|
- sig/rooibos/command.rbs
|
|
194
426
|
- sig/rooibos/error.rbs
|
|
195
427
|
- sig/rooibos/message.rbs
|
|
@@ -198,18 +430,20 @@ files:
|
|
|
198
430
|
- sig/rooibos/shortcuts.rbs
|
|
199
431
|
- sig/rooibos/test_helper.rbs
|
|
200
432
|
- sig/rooibos/version.rbs
|
|
433
|
+
- sig/rooibos/welcome.rbs
|
|
201
434
|
- tasks/example_viewer.html.erb
|
|
435
|
+
- tasks/install.rake
|
|
202
436
|
- tasks/resources/build.yml.erb
|
|
203
437
|
- tasks/resources/index.html.erb
|
|
204
438
|
- tasks/resources/rubies.yml
|
|
205
439
|
- tasks/steep.rake
|
|
206
440
|
- vendor/goodcop/base.yml
|
|
207
|
-
homepage: https://
|
|
441
|
+
homepage: https://rooibos.run
|
|
208
442
|
licenses:
|
|
209
443
|
- LGPL-3.0-or-later
|
|
210
444
|
metadata:
|
|
211
445
|
allowed_push_host: https://rubygems.org
|
|
212
|
-
homepage_uri: https://
|
|
446
|
+
homepage_uri: https://rooibos.run
|
|
213
447
|
bug_tracker_uri: https://todo.sr.ht/~kerrick/ratatui_ruby
|
|
214
448
|
mailing_list_uri: https://lists.sr.ht/~kerrick/ratatui_ruby-discuss
|
|
215
449
|
source_code_uri: https://git.sr.ht/~kerrick/rooibos
|
|
@@ -237,5 +471,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
237
471
|
requirements: []
|
|
238
472
|
rubygems_version: 4.0.3
|
|
239
473
|
specification_version: 4
|
|
240
|
-
summary:
|
|
474
|
+
summary: "☕ Confidently Build Terminal Apps"
|
|
241
475
|
test_files: []
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
-
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
|
-
-->
|
|
5
|
-
|
|
6
|
-
# Application Architecture
|
|
7
|
-
|
|
8
|
-
Build robust TUI applications with Rooibos patterns.
|
|
9
|
-
|
|
10
|
-
## Core Concepts
|
|
11
|
-
|
|
12
|
-
_This section is incomplete. Check the source files._
|
|
13
|
-
|
|
14
|
-
## Thread and Ractor Safety
|
|
15
|
-
|
|
16
|
-
### The Strategic Context
|
|
17
|
-
|
|
18
|
-
Ruby 4.0 introduces [Ractors](https://docs.ruby-lang.org/en/4.0/Ractor.html)—
|
|
19
|
-
true parallel actors that forbid shared mutable state. Code that passes
|
|
20
|
-
mutable objects between threads crashes in a Ractor world.
|
|
21
|
-
|
|
22
|
-
Rooibos prepares you today. The runtime enforces Ractor-shareability on every
|
|
23
|
-
Model and Message *now*, using standard threads. Pass a mutable object,
|
|
24
|
-
and it raises an error immediately. Write Ractor-safe code today; upgrade
|
|
25
|
-
to Ruby 4.0 without changes tomorrow.
|
|
26
|
-
|
|
27
|
-
Enforce immutability rules before you strictly need them, and the migration
|
|
28
|
-
is invisible.
|
|
29
|
-
|
|
30
|
-
### The Problem
|
|
31
|
-
|
|
32
|
-
Ruby's Ractor model prevents data races by forbidding shared mutable state.
|
|
33
|
-
Mutable objects cause runtime errors:
|
|
34
|
-
|
|
35
|
-
```
|
|
36
|
-
RatatuiRuby::Error::Invariant: Model is not Ractor-shareable.
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
### The Solution
|
|
40
|
-
|
|
41
|
-
Use `Ractor.make_shareable`. It recursively freezes everything:
|
|
42
|
-
|
|
43
|
-
```ruby
|
|
44
|
-
Ractor.make_shareable(model.with(text: "#{model.text}#{char}"))
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
For constants, wrap INITIAL:
|
|
48
|
-
|
|
49
|
-
```ruby
|
|
50
|
-
INITIAL = Ractor.make_shareable(
|
|
51
|
-
Model.new(text: "", running: false, chunks: [])
|
|
52
|
-
)
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
For collection updates:
|
|
56
|
-
|
|
57
|
-
```ruby
|
|
58
|
-
new_chunks = Ractor.make_shareable([*model.chunks, new_item])
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### The Lightweight Alternative
|
|
62
|
-
|
|
63
|
-
When you know exactly what's mutable, `.freeze` is shorter:
|
|
64
|
-
|
|
65
|
-
```ruby
|
|
66
|
-
[model.with(text: "#{model.text}#{char}".freeze), nil]
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
### Foot-Guns
|
|
70
|
-
|
|
71
|
-
#### frozen_string_literal Only Affects Literals
|
|
72
|
-
|
|
73
|
-
The magic comment freezes strings that appear directly in source code.
|
|
74
|
-
Computed strings are mutable.
|
|
75
|
-
|
|
76
|
-
```ruby
|
|
77
|
-
# frozen_string_literal: true
|
|
78
|
-
|
|
79
|
-
"literal" # frozen ✓
|
|
80
|
-
"#{var}" # mutable ✗
|
|
81
|
-
str.chop # mutable ✗
|
|
82
|
-
str + other # mutable ✗
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
#### Data.define Needs Shareable Values
|
|
86
|
-
|
|
87
|
-
`Data.define` creates frozen instances. The instance is Ractor-shareable
|
|
88
|
-
only when all its values are shareable.
|
|
89
|
-
|
|
90
|
-
### Quick Reference
|
|
91
|
-
|
|
92
|
-
| Pattern | Code |
|
|
93
|
-
|---------|------|
|
|
94
|
-
| Make anything shareable | `Ractor.make_shareable(obj)` |
|
|
95
|
-
| Freeze a string | `str.freeze` |
|
|
96
|
-
| INITIAL constant | `Ractor.make_shareable(Model.new(...))` |
|
|
97
|
-
| Array update | `Ractor.make_shareable([*old, new])` |
|
|
98
|
-
|
|
99
|
-
### Debugging
|
|
100
|
-
|
|
101
|
-
See this error?
|
|
102
|
-
|
|
103
|
-
```
|
|
104
|
-
RatatuiRuby::Error::Invariant: Model is not Ractor-shareable.
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
Wrap the returned model with `Ractor.make_shareable`.
|
|
108
|
-
|
|
109
|
-
## Modals and Command Result Routing
|
|
110
|
-
|
|
111
|
-
Modals capture keyboard input. They overlay the main UI and intercept keypresses until dismissed. But async commands keep running in the background. When their results arrive, they look like any other message.
|
|
112
|
-
|
|
113
|
-
It's tempting to intercept *all* messages when the modal is active. This swallows those command results.
|
|
114
|
-
|
|
115
|
-
### The Scenario
|
|
116
|
-
|
|
117
|
-
1. User presses "u" to fetch uptime. The runtime dispatches an async command.
|
|
118
|
-
2. User presses "c" to open a modal dialog.
|
|
119
|
-
3. The uptime command completes. It sends `[:network, :uptime, { stdout:, ... }]`.
|
|
120
|
-
4. The modal intercept sees the modal is active. It routes that message to the modal.
|
|
121
|
-
5. The modal ignores it.
|
|
122
|
-
6. The uptime panel never updates.
|
|
123
|
-
|
|
124
|
-
### The Fix
|
|
125
|
-
|
|
126
|
-
Route command results before modal interception. Modals intercept user input, not async results.
|
|
127
|
-
|
|
128
|
-
<!-- SPDX-SnippetBegin -->
|
|
129
|
-
<!--
|
|
130
|
-
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
131
|
-
SPDX-License-Identifier: MIT-0
|
|
132
|
-
-->
|
|
133
|
-
```ruby
|
|
134
|
-
# Wrong: modal intercepts everything
|
|
135
|
-
UPDATE = lambda do |message, model|
|
|
136
|
-
if Modal.active?(model.modal)
|
|
137
|
-
# Swallows command results
|
|
138
|
-
return Modal::UPDATE.call(message, model.modal)
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
case message
|
|
142
|
-
in [:network, *rest]
|
|
143
|
-
# Never reached while modal is open
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
# Correct: route command results first
|
|
148
|
-
UPDATE = lambda do |message, model|
|
|
149
|
-
# 1. Route async command results (always)
|
|
150
|
-
case message
|
|
151
|
-
in [:network, *rest]
|
|
152
|
-
return [model.with(network: new_network), command]
|
|
153
|
-
in [:stats, *rest]
|
|
154
|
-
return [model.with(stats: new_stats), command]
|
|
155
|
-
else
|
|
156
|
-
nil
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
# 2. Modal intercepts user input
|
|
160
|
-
if Modal.active?(model.modal)
|
|
161
|
-
return Modal::UPDATE.call(message, model.modal)
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
# 3. Handle other input
|
|
165
|
-
case message
|
|
166
|
-
in _ if message.q? then Command.exit
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
```
|
|
170
|
-
<!-- SPDX-SnippetEnd -->
|
|
171
|
-
|
|
172
|
-
### The Router DSL
|
|
173
|
-
|
|
174
|
-
`Rooibos::Router` handles this correctly. Routes declared with `route :prefix, to: ChildModule` process before keymap handlers. Command results flow through even when guards block keyboard input.
|
|
175
|
-
|
|
176
|
-
<!-- SPDX-SnippetBegin -->
|
|
177
|
-
<!--
|
|
178
|
-
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
179
|
-
SPDX-License-Identifier: MIT-0
|
|
180
|
-
-->
|
|
181
|
-
|
|
182
|
-
```ruby
|
|
183
|
-
|
|
184
|
-
module Dashboard
|
|
185
|
-
include Rooibos::Router
|
|
186
|
-
|
|
187
|
-
route :stats, to: StatsPanel
|
|
188
|
-
route :network, to: NetworkPanel
|
|
189
|
-
|
|
190
|
-
keymap do
|
|
191
|
-
only when: MODAL_INACTIVE do
|
|
192
|
-
key "u", -> { Uptime.fetch_command }
|
|
193
|
-
end
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
```
|
|
197
|
-
<!-- SPDX-SnippetEnd -->
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
-
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
|
-
-->
|
|
5
|
-
# Application Testing Guide
|
|
6
|
-
|
|
7
|
-
This guide explains how to test your RatatuiRuby applications using the provided `RatatuiRuby::TestHelper`.
|
|
8
|
-
|
|
9
|
-
## Overview
|
|
10
|
-
|
|
11
|
-
You need to verify that your application looks and behaves correctly. Manually checking every character on a terminal screen is tedious. Dealing with race conditions and complex state management in tests creates friction.
|
|
12
|
-
|
|
13
|
-
The `TestHelper` module solves this. It provides a headless "test terminal" to capture output and a suite of robust assertions to verify state.
|
|
14
|
-
|
|
15
|
-
Use it to write fast, deterministic tests for your TUI applications.
|
|
16
|
-
|
|
17
|
-
## Setup
|
|
18
|
-
|
|
19
|
-
First, require the test helper in your test file or `test_helper.rb`:
|
|
20
|
-
|
|
21
|
-
<!-- SPDX-SnippetBegin -->
|
|
22
|
-
<!--
|
|
23
|
-
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
24
|
-
SPDX-License-Identifier: MIT-0
|
|
25
|
-
-->
|
|
26
|
-
```ruby
|
|
27
|
-
require "ratatui_ruby/test_helper"
|
|
28
|
-
require "minitest/autorun" # or your preferred test framework
|
|
29
|
-
```
|
|
30
|
-
<!-- SPDX-SnippetEnd -->
|
|
31
|
-
|
|
32
|
-
Then, include the module in your test class:
|
|
33
|
-
|
|
34
|
-
<!-- SPDX-SnippetBegin -->
|
|
35
|
-
<!--
|
|
36
|
-
SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
37
|
-
SPDX-License-Identifier: MIT-0
|
|
38
|
-
-->
|
|
39
|
-
```ruby
|
|
40
|
-
class MyApplicationTest < Minitest::Test
|
|
41
|
-
include RatatuiRuby::TestHelper
|
|
42
|
-
# ...
|
|
43
|
-
end
|
|
44
|
-
```
|
|
45
|
-
<!-- SPDX-SnippetEnd -->
|
|
46
|
-
|
|
47
|
-
## Writing Tests
|
|
48
|
-
|
|
49
|
-
_Because this gem is in pre-release, it lacks documentation. Please check the source files and automated tests._
|