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.
Files changed (169) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +9 -5
  3. data/.builds/ruby-3.3.yml +9 -5
  4. data/.builds/ruby-3.4.yml +9 -5
  5. data/.builds/ruby-4.0.0.yml +9 -5
  6. data/AGENTS.md +1 -1
  7. data/CHANGELOG.md +46 -0
  8. data/README.md +2 -2
  9. data/README.rdoc +374 -0
  10. data/REUSE.toml +5 -0
  11. data/Rakefile +1 -1
  12. data/doc/best_practices/forms_and_validation.md +20 -0
  13. data/doc/best_practices/http_workflows.md +20 -0
  14. data/doc/best_practices/index.md +26 -0
  15. data/doc/best_practices/lists_and_tables.md +20 -0
  16. data/doc/best_practices/modal_dialogs.md +20 -0
  17. data/doc/best_practices/no_stateful_widgets.md +184 -0
  18. data/doc/best_practices/orchestration.md +20 -0
  19. data/doc/best_practices/streaming_data.md +20 -0
  20. data/doc/contributors/design/commands_and_outlets.md +1 -1
  21. data/doc/contributors/documentation_plan.md +616 -0
  22. data/doc/contributors/documentation_stub_audit.md +112 -0
  23. data/doc/contributors/documentation_style.md +275 -0
  24. data/doc/contributors/e2e_pty.md +168 -0
  25. data/doc/contributors/specs/earliest_tutorial_steps_per_story.md +70 -0
  26. data/doc/contributors/specs/file_browser.md +789 -0
  27. data/doc/contributors/specs/file_browser_stories.md +774 -0
  28. data/doc/contributors/specs/tutorials_to_stories.rb +167 -0
  29. data/doc/contributors/todo/scrollbar.md +118 -0
  30. data/doc/contributors/tutorial_old/01_project_setup.md +20 -0
  31. data/doc/contributors/tutorial_old/02_hello_world.md +24 -0
  32. data/doc/contributors/tutorial_old/03_adding_state.md +26 -0
  33. data/doc/contributors/tutorial_old/06_organizing_your_code.md +20 -0
  34. data/doc/contributors/tutorial_old/07_your_first_command.md +21 -0
  35. data/doc/contributors/tutorial_old/08_the_preview_pane.md +20 -0
  36. data/doc/contributors/tutorial_old/09_loading_states.md +20 -0
  37. data/doc/contributors/tutorial_old/10_testing_your_app.md +20 -0
  38. data/doc/contributors/tutorial_old/11_polish_and_refine.md +20 -0
  39. data/doc/contributors/tutorial_old/12_going_further.md +20 -0
  40. data/doc/contributors/tutorial_old/index.md +20 -0
  41. data/doc/essentials/commands.md +20 -0
  42. data/doc/essentials/index.md +31 -0
  43. data/doc/essentials/messages.md +21 -0
  44. data/doc/essentials/models.md +21 -0
  45. data/doc/essentials/shortcuts.md +19 -0
  46. data/doc/essentials/the_elm_architecture.md +24 -0
  47. data/doc/essentials/the_runtime.md +21 -0
  48. data/doc/essentials/update_functions.md +20 -0
  49. data/doc/essentials/views.md +22 -0
  50. data/doc/getting_started/for_go_developers.md +16 -0
  51. data/doc/getting_started/for_python_developers.md +16 -0
  52. data/doc/getting_started/for_react_developers.md +17 -0
  53. data/doc/getting_started/index.md +52 -0
  54. data/doc/getting_started/install.md +20 -0
  55. data/doc/getting_started/quickstart.md +9 -45
  56. data/doc/getting_started/ruby_primer.md +19 -0
  57. data/doc/getting_started/why_rooibos.md +20 -0
  58. data/doc/index.md +79 -11
  59. data/doc/scaling_up/async_patterns.md +20 -0
  60. data/doc/scaling_up/command_composition.md +20 -0
  61. data/doc/scaling_up/custom_commands.md +21 -0
  62. data/doc/scaling_up/fractal_architecture.md +20 -0
  63. data/doc/scaling_up/index.md +30 -0
  64. data/doc/scaling_up/message_routing.md +20 -0
  65. data/doc/scaling_up/ractor_safety.md +20 -0
  66. data/doc/scaling_up/testing.md +21 -0
  67. data/doc/troubleshooting/common_errors.md +20 -0
  68. data/doc/troubleshooting/debugging.md +21 -0
  69. data/doc/troubleshooting/index.md +23 -0
  70. data/doc/troubleshooting/performance.md +20 -0
  71. data/doc/tutorial/01_project_setup.md +44 -0
  72. data/doc/tutorial/02_hello_world.md +45 -0
  73. data/doc/tutorial/03_static_file_list.md +44 -0
  74. data/doc/tutorial/04_arrow_navigation.md +47 -0
  75. data/doc/tutorial/05_real_files.md +45 -0
  76. data/doc/tutorial/06_safe_refactoring.md +21 -0
  77. data/doc/tutorial/07_red_first_tdd.md +26 -0
  78. data/doc/tutorial/08_file_metadata.md +42 -0
  79. data/doc/tutorial/09_text_preview.md +44 -0
  80. data/doc/tutorial/10_directory_tree.md +42 -0
  81. data/doc/tutorial/11_pane_focus.md +40 -0
  82. data/doc/tutorial/12_sorting.md +41 -0
  83. data/doc/tutorial/13_filtering.md +43 -0
  84. data/doc/tutorial/14_toggle_hidden.md +41 -0
  85. data/doc/tutorial/15_text_input_widget.md +43 -0
  86. data/doc/tutorial/16_rename_files.md +42 -0
  87. data/doc/tutorial/17_confirmation_dialogs.md +43 -0
  88. data/doc/tutorial/18_progress_indicators.md +43 -0
  89. data/doc/tutorial/19_atomic_operations.md +42 -0
  90. data/doc/tutorial/20_external_editor.md +42 -0
  91. data/doc/tutorial/21_modal_overlays.md +41 -0
  92. data/doc/tutorial/22_error_handling.md +43 -0
  93. data/doc/tutorial/23_terminal_capabilities.md +53 -0
  94. data/doc/tutorial/24_mouse_events.md +43 -0
  95. data/doc/tutorial/25_resize_events.md +43 -0
  96. data/doc/tutorial/26_loading_states.md +42 -0
  97. data/doc/tutorial/27_performance.md +43 -0
  98. data/doc/tutorial/28_color_schemes.md +47 -0
  99. data/doc/tutorial/29_configuration.md +124 -0
  100. data/doc/tutorial/30_going_further.md +17 -0
  101. data/doc/tutorial/index.md +17 -0
  102. data/examples/app_file_browser/app.rb +40 -0
  103. data/examples/app_fractal_dashboard/dashboard/update_manual.rb +7 -7
  104. data/examples/app_fractal_dashboard/fragments/custom_shell_input.rb +5 -5
  105. data/examples/app_fractal_dashboard/fragments/custom_shell_modal.rb +1 -1
  106. data/examples/app_fractal_dashboard/fragments/disk_usage.rb +2 -2
  107. data/examples/app_fractal_dashboard/fragments/network_panel.rb +4 -4
  108. data/examples/app_fractal_dashboard/fragments/ping.rb +2 -2
  109. data/examples/app_fractal_dashboard/fragments/stats_panel.rb +4 -4
  110. data/examples/app_fractal_dashboard/fragments/system_info.rb +2 -2
  111. data/examples/app_fractal_dashboard/fragments/uptime.rb +2 -2
  112. data/examples/verify_website_first_app/app.rb +85 -0
  113. data/examples/verify_website_hello_mvu/app.rb +31 -0
  114. data/examples/widget_command_system/app.rb +15 -13
  115. data/exe/rooibos +10 -0
  116. data/generate_tutorial_stubs.rb +126 -0
  117. data/lib/rooibos/cli/commands/new.rb +373 -0
  118. data/lib/rooibos/cli/commands/run.rb +98 -0
  119. data/lib/rooibos/cli.rb +78 -0
  120. data/lib/rooibos/command/all.rb +25 -20
  121. data/lib/rooibos/command/batch.rb +26 -25
  122. data/lib/rooibos/command/custom.rb +84 -1
  123. data/lib/rooibos/command/http.rb +59 -55
  124. data/lib/rooibos/command/lifecycle.rb +5 -5
  125. data/lib/rooibos/command/open.rb +86 -0
  126. data/lib/rooibos/command/outlet.rb +105 -3
  127. data/lib/rooibos/command/wait.rb +5 -5
  128. data/lib/rooibos/command.rb +57 -74
  129. data/lib/rooibos/message/batch.rb +39 -0
  130. data/lib/rooibos/message/canceled.rb +51 -0
  131. data/lib/rooibos/message/error.rb +48 -0
  132. data/lib/rooibos/message/open.rb +30 -0
  133. data/lib/rooibos/message.rb +84 -4
  134. data/lib/rooibos/router.rb +11 -14
  135. data/lib/rooibos/runtime.rb +40 -43
  136. data/lib/rooibos/shortcuts.rb +47 -0
  137. data/lib/rooibos/test_helper.rb +71 -6
  138. data/lib/rooibos/version.rb +1 -1
  139. data/lib/rooibos/welcome.rb +237 -0
  140. data/lib/rooibos.rb +4 -3
  141. data/mise.toml +1 -1
  142. data/rbs_collection.lock.yaml +2 -2
  143. data/sig/concurrent.rbs +3 -0
  144. data/sig/gem.rbs +20 -0
  145. data/sig/rooibos/cli.rbs +42 -0
  146. data/sig/rooibos/command.rbs +48 -0
  147. data/sig/rooibos/message.rbs +60 -0
  148. data/sig/rooibos/shortcuts.rbs +14 -0
  149. data/sig/rooibos/test_helper.rbs +6 -2
  150. data/sig/rooibos/welcome.rbs +75 -0
  151. data/tasks/install.rake +29 -0
  152. data/tasks/resources/build.yml.erb +2 -0
  153. metadata +272 -38
  154. data/doc/concepts/application_architecture.md +0 -197
  155. data/doc/concepts/application_testing.md +0 -49
  156. data/doc/concepts/async_work.md +0 -164
  157. data/doc/concepts/commands.md +0 -530
  158. data/doc/concepts/message_processing.md +0 -51
  159. data/doc/contributors/WIP/decomposition_strategies_analysis.md +0 -258
  160. data/doc/contributors/WIP/implementation_plan.md +0 -409
  161. data/doc/contributors/WIP/init_callable_proposal.md +0 -344
  162. data/doc/contributors/WIP/runtime_refactoring_status.md +0 -47
  163. data/doc/contributors/WIP/task.md +0 -36
  164. data/doc/contributors/WIP/v0.4.0_todo.md +0 -468
  165. data/doc/contributors/kit-no-outlet.md +0 -238
  166. data/doc/contributors/priorities.md +0 -38
  167. data/doc/images/.gitkeep +0 -0
  168. data/exe/.gitkeep +0 -0
  169. /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.5.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: 0.10.1
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: 0.10.1
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
- description: Rooibos - part of the RatatuiRuby TUI framework ecosystem
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
- - ".gitkeep"
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/concepts/application_architecture.md
124
- - doc/concepts/application_testing.md
125
- - doc/concepts/async_work.md
126
- - doc/concepts/commands.md
127
- - doc/concepts/message_processing.md
128
- - doc/contributors/WIP/decomposition_strategies_analysis.md
129
- - doc/contributors/WIP/implementation_plan.md
130
- - doc/contributors/WIP/init_callable_proposal.md
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/kit-no-outlet.md
137
- - doc/contributors/priorities.md
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/images/.gitkeep
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/.gitkeep
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://sr.ht/~kerrick/ratatui_ruby/
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://sr.ht/~kerrick/ratatui_ruby/
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: Part of the RatatuiRuby ecosystem
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._