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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f7a4f2a869c06e70b95a34cac8915f8d8e6543283007fd7c595692cf34bf3329
4
- data.tar.gz: c8c0be8e5059b55671baa49105aa7b470dfdfa17647353f4e81ebcf06ecf7f15
3
+ metadata.gz: 3c2eb32007ddd93c0a653829b44492878ca229fef1f90c84516d474d3f324132
4
+ data.tar.gz: f52b7a94243bd9582b7229cf48b3834d3f9ae6abaf9f9d6c622625f1d3951a8a
5
5
  SHA512:
6
- metadata.gz: 97556fd7362f24d3d5572da7b11485456b3066bc56ef77994c2448a62085aa62d54c8503c5ffa2cea4472dd556bff1328ececb1dcb0c323cf663fdd1267ac8c9
7
- data.tar.gz: c02267ab34497516ee6b6fee6daab08b72fcf26f6d4499aaf5d98162df1f063ee7eeec0464361189d3371a77434cf94d72b030289acea29cbc0d4b972691f1aa
6
+ metadata.gz: 2d5dc50320f2b8026cbb4f33d4c94b2a18beb6930bbdda45a0d38f14013c7015ecb5fce740d2003e6ba19cf48c901dc21e65034d2a66cc6ab6e6c7a3615688e0
7
+ data.tar.gz: 17a5f9bd24f5c9075fbe333a542a094ec40aa9063bd93d0265a5c4c264ead213d9c5030691c62cfd40c3c731570b5c30da0665dc67dbb1ff2a57545e729404f4
data/.builds/ruby-3.2.yml CHANGED
@@ -15,8 +15,10 @@ packages:
15
15
  - libffi
16
16
  - clang
17
17
  - git
18
+ artifacts:
19
+ - rooibos/pkg/rooibos-0.6.0.gem
18
20
  sources:
19
- - https://git.sr.ht/~kerrick/ratatui_ruby-tea
21
+ - https://git.sr.ht/~kerrick/rooibos
20
22
  tasks:
21
23
  - setup: |
22
24
  curl https://mise.jdx.dev/install.sh | sh
@@ -27,7 +29,7 @@ tasks:
27
29
  echo 'export BINDGEN_EXTRA_CLANG_ARGS="-include stdbool.h"' >> ~/.buildenv
28
30
  . ~/.buildenv
29
31
  export CI="true"
30
- cd ratatui_ruby-tea
32
+ cd rooibos
31
33
  sed -i 's/ruby = .*/ruby = "3.2"/' mise.toml
32
34
  mise install
33
35
  mise x -- pip install reuse
@@ -37,15 +39,17 @@ tasks:
37
39
  mise x -- bundle install
38
40
  - test: |
39
41
  . ~/.buildenv
40
- cd ratatui_ruby-tea
42
+ cd rooibos
43
+ echo "Installing rooibos gem globally for integration tests..."
44
+ mise x -- bundle exec rake install:force
41
45
  echo "Testing Ruby 3.2"
42
46
  mise x -- bundle exec rake test
43
47
  - lint: |
44
48
  . ~/.buildenv
45
- cd ratatui_ruby-tea
49
+ cd rooibos
46
50
  echo "Linting Ruby 3.2"
47
51
  mise x -- bundle exec rake lint
48
52
  - package: |
49
53
  . ~/.buildenv
50
- cd ratatui_ruby-tea
54
+ cd rooibos
51
55
  mise x -- bundle exec rake build
data/.builds/ruby-3.3.yml CHANGED
@@ -15,8 +15,10 @@ packages:
15
15
  - libffi
16
16
  - clang
17
17
  - git
18
+ artifacts:
19
+ - rooibos/pkg/rooibos-0.6.0.gem
18
20
  sources:
19
- - https://git.sr.ht/~kerrick/ratatui_ruby-tea
21
+ - https://git.sr.ht/~kerrick/rooibos
20
22
  tasks:
21
23
  - setup: |
22
24
  curl https://mise.jdx.dev/install.sh | sh
@@ -27,7 +29,7 @@ tasks:
27
29
  echo 'export BINDGEN_EXTRA_CLANG_ARGS="-include stdbool.h"' >> ~/.buildenv
28
30
  . ~/.buildenv
29
31
  export CI="true"
30
- cd ratatui_ruby-tea
32
+ cd rooibos
31
33
  sed -i 's/ruby = .*/ruby = "3.3"/' mise.toml
32
34
  mise install
33
35
  mise x -- pip install reuse
@@ -37,15 +39,17 @@ tasks:
37
39
  mise x -- bundle install
38
40
  - test: |
39
41
  . ~/.buildenv
40
- cd ratatui_ruby-tea
42
+ cd rooibos
43
+ echo "Installing rooibos gem globally for integration tests..."
44
+ mise x -- bundle exec rake install:force
41
45
  echo "Testing Ruby 3.3"
42
46
  mise x -- bundle exec rake test
43
47
  - lint: |
44
48
  . ~/.buildenv
45
- cd ratatui_ruby-tea
49
+ cd rooibos
46
50
  echo "Linting Ruby 3.3"
47
51
  mise x -- bundle exec rake lint
48
52
  - package: |
49
53
  . ~/.buildenv
50
- cd ratatui_ruby-tea
54
+ cd rooibos
51
55
  mise x -- bundle exec rake build
data/.builds/ruby-3.4.yml CHANGED
@@ -15,8 +15,10 @@ packages:
15
15
  - libffi
16
16
  - clang
17
17
  - git
18
+ artifacts:
19
+ - rooibos/pkg/rooibos-0.6.0.gem
18
20
  sources:
19
- - https://git.sr.ht/~kerrick/ratatui_ruby-tea
21
+ - https://git.sr.ht/~kerrick/rooibos
20
22
  tasks:
21
23
  - setup: |
22
24
  curl https://mise.jdx.dev/install.sh | sh
@@ -27,7 +29,7 @@ tasks:
27
29
  echo 'export BINDGEN_EXTRA_CLANG_ARGS="-include stdbool.h"' >> ~/.buildenv
28
30
  . ~/.buildenv
29
31
  export CI="true"
30
- cd ratatui_ruby-tea
32
+ cd rooibos
31
33
  sed -i 's/ruby = .*/ruby = "3.4"/' mise.toml
32
34
  mise install
33
35
  mise x -- pip install reuse
@@ -37,15 +39,17 @@ tasks:
37
39
  mise x -- bundle install
38
40
  - test: |
39
41
  . ~/.buildenv
40
- cd ratatui_ruby-tea
42
+ cd rooibos
43
+ echo "Installing rooibos gem globally for integration tests..."
44
+ mise x -- bundle exec rake install:force
41
45
  echo "Testing Ruby 3.4"
42
46
  mise x -- bundle exec rake test
43
47
  - lint: |
44
48
  . ~/.buildenv
45
- cd ratatui_ruby-tea
49
+ cd rooibos
46
50
  echo "Linting Ruby 3.4"
47
51
  mise x -- bundle exec rake lint
48
52
  - package: |
49
53
  . ~/.buildenv
50
- cd ratatui_ruby-tea
54
+ cd rooibos
51
55
  mise x -- bundle exec rake build
@@ -15,8 +15,10 @@ packages:
15
15
  - libffi
16
16
  - clang
17
17
  - git
18
+ artifacts:
19
+ - rooibos/pkg/rooibos-0.6.0.gem
18
20
  sources:
19
- - https://git.sr.ht/~kerrick/ratatui_ruby-tea
21
+ - https://git.sr.ht/~kerrick/rooibos
20
22
  tasks:
21
23
  - setup: |
22
24
  curl https://mise.jdx.dev/install.sh | sh
@@ -27,7 +29,7 @@ tasks:
27
29
  echo 'export BINDGEN_EXTRA_CLANG_ARGS="-include stdbool.h"' >> ~/.buildenv
28
30
  . ~/.buildenv
29
31
  export CI="true"
30
- cd ratatui_ruby-tea
32
+ cd rooibos
31
33
  sed -i 's/ruby = .*/ruby = "4.0.0"/' mise.toml
32
34
  mise install
33
35
  mise x -- pip install reuse
@@ -37,15 +39,17 @@ tasks:
37
39
  mise x -- bundle install
38
40
  - test: |
39
41
  . ~/.buildenv
40
- cd ratatui_ruby-tea
42
+ cd rooibos
43
+ echo "Installing rooibos gem globally for integration tests..."
44
+ mise x -- bundle exec rake install:force
41
45
  echo "Testing Ruby 4.0.0"
42
46
  mise x -- bundle exec rake test
43
47
  - lint: |
44
48
  . ~/.buildenv
45
- cd ratatui_ruby-tea
49
+ cd rooibos
46
50
  echo "Linting Ruby 4.0.0"
47
51
  mise x -- bundle exec rake lint
48
52
  - package: |
49
53
  . ~/.buildenv
50
- cd ratatui_ruby-tea
54
+ cd rooibos
51
55
  mise x -- bundle exec rake build
data/AGENTS.md CHANGED
@@ -35,7 +35,7 @@ Description: Part of the RatatuiRuby ecosystem.
35
35
 
36
36
  - **BANNED WORD: "component"** — Reserved for Kit.
37
37
  - **Avoid "widget" for Rooibos units** — "Widget" refers to Engine/Ratatui render primitives. In Rooibos, call them **fragments**.
38
- - **Fragment:** A module containing `Model`, `INITIAL`, `UPDATE`, and `VIEW` constants. Fragments compose: parent fragments delegate to child fragments.
38
+ - **Fragment:** A module containing `Model`, `Init`, `Update`, and `View` constants. Fragments compose: parent fragments delegate to child fragments.
39
39
  - Use "model", "update", "view" for the MVU pattern. Use "message" (not "msg") and "command" (not "cmd").
40
40
 
41
41
  ### Ruby Standards
data/CHANGELOG.md CHANGED
@@ -21,6 +21,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
21
21
 
22
22
  ### Removed
23
23
 
24
+ ## [0.6.0] - 2026-01-25
25
+
26
+ ### Added
27
+
28
+ - **rooibos CLI**: New command-line interface installed as executable when you install the gem. Provides `rooibos new APP_NAME` to scaffold a complete Rooibos application using `bundle gem` conventions, and `rooibos run` to launch the application. The scaffolded app includes a working TUI that displays "Hello, Rooibos!" and exits on q or Ctrl+C, plus a passing test demonstrating `Rooibos::TestHelper` patterns.
29
+
30
+ - **Rooibos::Welcome**: Built-in welcome screen fragment available via `require "rooibos/welcome"`. Provides `Model`, `View`, `Update`, and `Init` constants that scaffolded apps delegate to. Features keyboard Tab/Shift+Tab focus cycling, mouse hover states, and clickable buttons for visiting the website or exiting. Use as a starting point or reference for building your own fragments.
31
+
32
+ - **Command::Custom#deconstruct_keys**: Default pattern matching support for custom commands. Introspects public query methods and returns a hash with `:type` as a snake_case discriminator. Data.define members are included automatically. Respects the `keys` argument for performance optimization. Override for hot paths or metaprogrammed methods.
33
+
34
+ - **Rooibos::TestHelper#assert_no_errors**: Test assertion to fail fast when `Message::Error` is unexpectedly present in collected messages. Works with Minitest (via `flunk`) and RSpec (via `raise`). Include via `include Rooibos::TestHelper`.
35
+
36
+ - **Message::Error**: New message type for command errors. Includes `error?` predicate and `deconstruct_keys` for pattern matching with `{ type: :error, command:, exception: }`.
37
+
38
+ - **Message::Canceled**: New message type for canceled commands. Includes `canceled?` predicate (with `cancelled?` alias for British spelling) and `deconstruct_keys` for pattern matching with `{ type: :canceled, command: }`. Custom command authors should emit this when `token.canceled?` is true.
39
+
40
+ - **Message Symbol Comparison**: All `Message::*` types now support symbol comparison via `to_sym` and `==`, similar to RatatuiRuby events. Symbols use the `message_` prefix to avoid collision with event types: `msg == :message_timer`, `msg == :message_http`, `msg == :message_error`. `Message::Predicates` also provides a smart-default `deconstruct_keys` that derives `:type` from the class name.
41
+
42
+ - **Rooibos::Message.=== for case/when dispatch**: The `Rooibos::Message` module now implements `===` for use in case/when statements. Matches only built-in framework message types (classes under `Rooibos::Message::`), rejecting key events and user-defined message classes. Enables Update functions to distinguish framework responses from user input.
43
+
44
+ - **Command.open**: Opens a file or URL with the system's default application. Cross-platform: uses `open` on macOS, `xdg-open` on Linux, `start` on Windows. Sends `Message::Open` on success (exit 0) or `Message::Error` on failure.
45
+
46
+ ### Changed
47
+
48
+ - **BREAKING: Rooibos::TestHelper Include Pattern**: `Rooibos::TestHelper` now includes `RatatuiRuby::TestHelper` instead of the other way around. Previously, requiring `rooibos/test_helper` would inject Rooibos assertions into `RatatuiRuby::TestHelper`. Now, use `include Rooibos::TestHelper` to get both Rooibos assertions and RatatuiRuby test terminal helpers. Update your test classes from `include RatatuiRuby::TestHelper` to `include Rooibos::TestHelper`.
49
+
50
+ - **BREAKING: Rooibos.delegate Message Format**: `Rooibos.delegate` now passes `message[1]` (single value) to child UPDATEs instead of `message[1..]` (array slice). This aligns child fragments with the universal `{ type:, envelope: }` pattern. Update pattern matches from `in [{ type: :system, ... }]` to `in { type: :system, ... }`.
51
+
52
+ - **BREAKING: Command::Error → Message::Error**: Moved `Command::Error` to `Message::Error`. Commands flow *out* from Update; Messages flow *in* to Update. Error was always sent *to* Update so it belongs in the Message module. Includes `error?` predicate and `deconstruct_keys` for pattern matching with `{ type: :error, command:, exception: }`. Update pattern matches from `Command::Error` to `Message::Error`.
53
+
54
+ - **BREAKING: Timer/Batch Cancellation → Message::Canceled**: When `Command.wait`, `Command.tick`, `Command.all`, or `Command.batch` are canceled, they now send `Message::Canceled` instead of `Command.cancel(self)`. Custom command authors should do the same: when `token.canceled?`, emit `Message::Canceled.new(command: self)`. Update pattern matches from `in Command::Cancel` to `in Message::Canceled` or `in { type: :canceled, command: }`.
55
+
56
+ - **Dependency Update**: Now requires `ratatui_ruby ~> 1.2.0` (was `~> 1.0.0.beta.3`). This stable release adds inline sync mode for deterministic event ordering in tests.
57
+
58
+ ### Fixed
59
+
60
+ - **Init runs after terminal initialization**: `Init` callables now run after the terminal is initialized, enabling them to call `RatatuiRuby.terminal_size`, compute layout areas, or perform other terminal-dependent initialization. Previously Init ran before the terminal was ready, causing "Terminal is not initialized" errors.
61
+
62
+ - **FPS timeout uses float division**: The runtime now uses `1.0 / fps` instead of `1 / fps` for poll timeout calculation. Integer division caused `1 / 60` to yield 0, resulting in busy-wait CPU spinning at 100%.
63
+
64
+ ### Removed
65
+
66
+ - **BREAKING: Command::Error class**: Removed. Use `Message::Error` instead.
67
+ - **BREAKING: Command.error factory**: Removed. Use `Message::Error.new(command:, exception:)` instead.
68
+
24
69
  ## [0.5.0] - 2026-01-16
25
70
 
26
71
  ### Added
@@ -205,6 +250,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
205
250
  - **First Release**: Empty release of `rooibos`, a Ruby implementation of The Elm Architecture (TEA) for `ratatui_ruby`. Scaffolding generated by `ratatui_ruby-devtools`.
206
251
 
207
252
  [Unreleased]: https://git.sr.ht/~kerrick/rooibos/refs/HEAD
253
+ [0.6.0]: https://git.sr.ht/~kerrick/rooibos/refs/v0.6.0
208
254
  [0.5.0]: https://git.sr.ht/~kerrick/rooibos/refs/v0.5.0
209
255
  [0.4.0]: https://git.sr.ht/~kerrick/rooibos/refs/v0.4.0
210
256
  [0.3.1]: https://git.sr.ht/~kerrick/rooibos/refs/v0.3.1
data/README.md CHANGED
@@ -20,7 +20,7 @@ Mailing List: Announcements](https://img.shields.io/badge/mailing_list-announcem
20
20
  **ratatui_ruby** is a community wrapper that is not affiliated with [the Ratatui team](https://github.com/orgs/ratatui/people).
21
21
 
22
22
  > [!WARNING]
23
- > **rooibos** is currently in **ALPHA**. The API may change with minor versions.
23
+ > **rooibos** is currently in **BETA**. The API may change with minor versions.
24
24
 
25
25
  **[Why RatatuiRuby?](https://man.sr.ht/~kerrick/ratatui_ruby/why.md)** — Native Rust performance, zero runtime overhead, and Ruby's expressiveness. [See how we compare](https://man.sr.ht/~kerrick/ratatui_ruby/why.md) to CharmRuby, raw Rust, and Go.
26
26
 
@@ -146,7 +146,7 @@ For a full tutorial, see [the Quickstart](./doc/getting_started/quickstart.md).
146
146
 
147
147
  ## Features
148
148
 
149
- _Because this gem is in pre-release, it lacks documentation. Please check the source files.
149
+ _Because this gem is in alpha, it lacks documentation. Please check the source files._
150
150
 
151
151
 
152
152
  ## Documentation
data/README.rdoc ADDED
@@ -0,0 +1,374 @@
1
+ == Confidently Build Terminal Apps
2
+
3
+ Rooibos[https://rooibos.run] helps you build interactive terminal applications.
4
+ Keep your code understandable and testable as it scales. Rooibos handles
5
+ keyboard, mouse, and async work so you can focus on behavior and user experience.
6
+
7
+ gem install rooibos
8
+
9
+ <i>Currently in beta. APIs may change before 1.0.</i>
10
+
11
+ === Get Started in Seconds
12
+
13
+ rooibos new my_app
14
+ cd my_app
15
+ rooibos run
16
+
17
+ That's it. You have a working app with keyboard navigation, mouse support,
18
+ and clickable buttons. Open <tt>lib/my_app.rb</tt> to make it your own.
19
+
20
+
21
+ ---
22
+
23
+ === The Pattern
24
+
25
+ \Rooibos uses Model-View-Update, the architecture behind
26
+ Elm[https://guide.elm-lang.org/architecture/],
27
+ Redux[https://redux.js.org/], and {Bubble
28
+ Tea}[https://github.com/charmbracelet/bubbletea].
29
+ State lives in one place. Updates flow in one direction. The runtime handles
30
+ rendering and runs background work for you.
31
+
32
+ ---
33
+
34
+ === Hello, MVU
35
+
36
+ The simplest \Rooibos app. Press any key to increment the counter. Press
37
+ <tt>Ctrl</tt>+<tt>C</tt> to quit.
38
+
39
+ require "rooibos"
40
+
41
+ module Counter
42
+ # Init: How do you create the initial model?
43
+ Init = -> { 0 }
44
+
45
+ # View: What does the user see?
46
+ View = -> (model, tui) { tui.paragraph(text: <<~END) }
47
+ Current count: #{model}.
48
+ Press any key to increment.
49
+ Press Ctrl+C to quit.
50
+ END
51
+
52
+ # Update: What happens when things change?
53
+ Update = -> (message, model) {
54
+ if message.ctrl_c?
55
+ Rooibos::Command.exit
56
+ elsif message.key?
57
+ model + 1
58
+ end
59
+ }
60
+ end
61
+
62
+ Rooibos.run(Counter)
63
+
64
+ That's the whole pattern: Model holds state, Init creates it, View renders it,
65
+ and Update changes it. The runtime handles everything else.
66
+
67
+
68
+ ---
69
+
70
+ === Your First Real Application
71
+
72
+ A file browser in sixty lines. It opens files, navigates directories, handles
73
+ errors, styles directories and hidden files differently, and supports vim-style
74
+ keyboard shortcuts. If you can do this much with this little code, imagine how
75
+ easy _your_ app will be to build.
76
+
77
+ require "rooibos"
78
+
79
+ module FileBrowser
80
+ # Model: What state does your app need?
81
+ Model = Data.define(:path, :entries, :selected, :error)
82
+
83
+ Init = -> {
84
+ path = Dir.pwd
85
+ entries = Entries[path]
86
+ Ractor.make_shareable( # Ensures thread safety
87
+ Model.new(path:, entries:, selected: entries.first, error: nil))
88
+ }
89
+
90
+ View = -> (model, tui) {
91
+ tui.block(
92
+ titles: [model.error || model.path,
93
+ { content: KEYS, position: :bottom, alignment: :right}],
94
+ borders: [:all],
95
+ border_style: if model.error then tui.style(fg: :red) else nil end,
96
+ children: [tui.list(items: model.entries.map(&ListItem[model, tui]),
97
+ selected_index: model.entries.index(model.selected),
98
+ highlight_symbol: "",
99
+ highlight_style: tui.style(modifiers: [:reversed]))]
100
+ )
101
+ }
102
+
103
+ Update = -> (message, model) {
104
+ return model.with(error: ERROR) if message.error?
105
+ model = model.with(error: nil) if model.error && message.key?
106
+
107
+ if message.ctrl_c? || message.q? then Rooibos::Command.exit
108
+ elsif message.home? || message.g? then model.with(selected: model.entries.first)
109
+ elsif message.end? || message.G? then model.with(selected: model.entries.last)
110
+ elsif message.up_arrow? || message.k? then Select[:-, model]
111
+ elsif message.down_arrow? || message.j? then Select[:+, model]
112
+ elsif message.enter? then Open[model]
113
+ elsif message.escape? then Navigate[File.dirname(model.path), model]
114
+ end
115
+ }
116
+
117
+ private # Lines below this are implementation details
118
+
119
+ KEYS = "↑/↓/Home/End: Select | Enter: Open | Esc: Navigate Up | q: Quit"
120
+ ERROR = "Sorry, opening the selected file failed."
121
+
122
+ ListItem = -> (model, tui) { -> (name) {
123
+ modifiers = name.start_with?(".") ? [:dim] : []
124
+ fg = :blue if name.end_with?("/")
125
+ tui.list_item(content: name, style: tui.style(fg:, modifiers:))
126
+ } }
127
+
128
+ Select = -> (operator, model) {
129
+ new_index = model.entries.index(model.selected).public_send(operator, 1)
130
+ model.with(selected: model.entries[new_index.clamp(0, model.entries.length - 1)])
131
+ }
132
+
133
+ Open = -> (model) {
134
+ full = File.join(model.path, model.selected.delete_suffix("/"))
135
+ model.selected.end_with?("/") ? Navigate[full, model] : Rooibos::Command.open(full)
136
+ }
137
+
138
+ Navigate = -> (path, model) {
139
+ entries = Entries[path]
140
+ model.with(path:, entries:, selected: entries.first, error: nil)
141
+ }
142
+
143
+ Entries = -> (path) {
144
+ Dir.children(path).map { |name|
145
+ File.directory?(File.join(path, name)) ? "#{name}/" : name
146
+ }.sort_by { |name| [name.end_with?("/") ? 0 : 1, name.downcase] }
147
+ }
148
+ end
149
+
150
+ Rooibos.run(FileBrowser)
151
+
152
+
153
+ ---
154
+
155
+ === Batteries Included
156
+
157
+ ==== Commands
158
+
159
+ Applications fetch data, run shell commands, and set timers. \Rooibos Commands
160
+ run off the main thread and send results back as messages.
161
+
162
+ <b>HTTP requests:</b>
163
+
164
+ Update = -> (message, model) {
165
+ case message
166
+ in :fetch_users
167
+ [model.with(loading: true), Rooibos::Command.http(:get, "/api/users", :got_users)]
168
+ in { type: :http, envelope: :got_users, status: 200, body: }
169
+ model.with(loading: false, users: JSON.parse(body))
170
+ in { type: :http, envelope: :got_users, status: }
171
+ model.with(error: "HTTP #{status}")
172
+ end
173
+ }
174
+
175
+ <b>Shell commands:</b>
176
+
177
+ Update = -> (message, model) {
178
+ case message
179
+ in :list_files
180
+ Rooibos::Command.system("ls -la", :listed_files)
181
+ in { type: :system, envelope: :listed_files, stdout:, status: 0 }
182
+ model.with(files: stdout.lines.map(&:chomp))
183
+ in { type: :system, envelope: :listed_files, stderr:, status: }
184
+ model.with(error: stderr)
185
+ end
186
+ }
187
+
188
+ <b>Timers:</b>
189
+
190
+ Update = -> (message, model) {
191
+ case message
192
+ in { type: :timer, envelope: :tick, elapsed: }
193
+ [model.with(frame: model.frame + 1), Rooibos::Command.wait(1.0 / 24, :tick)]
194
+ end
195
+ }
196
+
197
+ <b>And more!</b> \Rooibos includes <tt>all</tt>, <tt>batch</tt>, <tt>cancel</tt>,
198
+ <tt>custom</tt>, <tt>exit</tt>, <tt>http</tt>, <tt>map</tt>, <tt>open</tt>,
199
+ <tt>system</tt>, <tt>tick</tt>, and <tt>wait</tt> commands. You can also define
200
+ your own custom commands for complex orchestration.
201
+
202
+ Every command produces a message, and Update handles it the same way.
203
+
204
+ ==== Testing
205
+
206
+ \Rooibos makes TUIs so easy to test, you'll save more time by writing tests than
207
+ by not testing.
208
+
209
+ <b>Unit test Update, View, and Init.</b> No terminal needed. Test helpers included.
210
+
211
+ def test_moves_selection_down_with_j
212
+ model = Ractor.make_shareable(FileBrowser::Model.new(
213
+ path: "/", entries: %w[bin exe lib], selected: "bin", error: nil))
214
+ message = RatatuiRuby::Event::Key.new(code: "j")
215
+
216
+ result = FileBrowser::Update.call(message, model)
217
+
218
+ assert_equal "exe", result.selected
219
+ end
220
+
221
+ <b>Style assertions.</b> Draw to a headless terminal, verify colors and modifiers.
222
+
223
+ def test_directories_are_blue
224
+ with_test_terminal(60, 10) do
225
+ model = Ractor.make_shareable(FileBrowser::Model.new(
226
+ path: "/", entries: %w[file.txt subdir/], selected: "file.txt", error: nil))
227
+ widget = FileBrowser::View.call(model, RatatuiRuby::TUI.new)
228
+
229
+ RatatuiRuby.draw { |frame| frame.render_widget(widget, frame.area) }
230
+
231
+ assert_blue(1, 2) # "subdir/" at column 1, row 2
232
+ end
233
+ end
234
+
235
+ <b>System tests.</b> Inject events, run the full app, snapshot the result.
236
+
237
+ def test_selection_moves_down
238
+ with_test_terminal(120, 30) do
239
+ Dir.mktmpdir do |dir|
240
+ FileUtils.touch(File.join(dir, "a"))
241
+ FileUtils.touch(File.join(dir, "b"))
242
+ FileUtils.touch(File.join(dir, "c"))
243
+
244
+ inject_key(:down)
245
+ inject_key(:ctrl_c)
246
+
247
+ # Tests use explicit params to inject deterministic initial state.
248
+ Rooibos.run(
249
+ model: Ractor.make_shareable(FileBrowser::Model.new(
250
+ path: dir, entries: %w[a b c], selected: "a", error: nil)),
251
+ view: FileBrowser::View,
252
+ update: FileBrowser::Update
253
+ )
254
+
255
+ assert_snapshots("selection_moved_down") do |lines|
256
+ title = "┌/tmp/test#{'─' * 107}┐"
257
+ lines.map do |l|
258
+ l.gsub(/┌#{Regexp.escape(dir)}[^┐]*┐/, title)
259
+ end
260
+ end
261
+ end
262
+ end
263
+ end
264
+
265
+ Snapshots record both plain text and ANSI colors. Normalization blocks mask
266
+ dynamic content (timestamps, temp paths) for cross-platform reproducibility.
267
+ Run <tt>UPDATE_SNAPSHOTS=1 rake test</tt> to regenerate baselines.
268
+
269
+ ==== Scale Up
270
+
271
+ Large applications decompose into fragments. Each fragment has its own Model,
272
+ View, Update, and Init. Parents compose children. The pattern scales.
273
+
274
+ The Router DSL eliminates boilerplate:
275
+
276
+ module Dashboard
277
+ include Rooibos::Router
278
+
279
+ route :stats, to: StatsPanel
280
+ route :network, to: NetworkPanel
281
+
282
+ keymap do
283
+ key :ctrl_c, -> { Rooibos::Command.exit }
284
+ only when: -> (model) { !model.modal_open } do
285
+ key :q, -> { Rooibos::Command.exit }
286
+ key :s, -> { StatsPanel.fetch_command }
287
+ key :p, -> { NetworkPanel.ping_command }
288
+ end
289
+ end
290
+
291
+ Update = from_router
292
+
293
+ # ... Model, Init, View below
294
+ end
295
+
296
+ Declare routes and keymaps. The router generates Update for you. Use guards to
297
+ ignore keys when needed.
298
+
299
+ ==== CLI
300
+
301
+ The <tt>rooibos</tt> command scaffolds projects and runs applications.
302
+
303
+ rooibos new my_app # Generate project structure
304
+ rooibos run # Run the app in current directory
305
+
306
+ Generated apps include tests, type signatures, and a working welcome
307
+ screen with keyboard and mouse support.
308
+
309
+
310
+ ---
311
+
312
+ === The Ecosystem
313
+
314
+ \Rooibos builds on RatatuiRuby[https://www.ratatui-ruby.dev], a Rubygem built on
315
+ Ratatui[https://ratatui.rs]. You get native performance with the joy of Ruby.
316
+ \Rooibos is one way to manage state and composition. Kit is another.
317
+
318
+ ==== Rooibos[https://git.sr.ht/~kerrick/rooibos]
319
+
320
+ Model-View-Update architecture. Inspired by Elm, Bubble Tea, and React +
321
+ Redux. Your UI is a pure function of state.
322
+
323
+ - Functional programming with MVU
324
+ - Commands work off the main thread
325
+ - Messages, not callbacks, drive updates
326
+
327
+ ==== {Kit}[https://sr.ht/~kerrick/ratatui_ruby/#chapter-3-the-object-path--kit] (Coming Soon)
328
+
329
+ Component-based architecture. Encapsulate state, input handling, and
330
+ rendering in reusable pieces.
331
+
332
+ - OOP with stateful components
333
+ - Separate UI state from domain logic
334
+ - Built-in focus management & click handling
335
+
336
+ Both use the same widget library and rendering engine. Pick the paradigm
337
+ that fits your brain.
338
+
339
+
340
+ ---
341
+
342
+ === Links
343
+
344
+ [Get Started]
345
+ {Getting Started}[https://git.sr.ht/~kerrick/rooibos/tree/trunk/item/doc/getting_started/index.md],
346
+ {Tutorial}[https://git.sr.ht/~kerrick/rooibos/tree/trunk/item/doc/tutorial/index.md],
347
+ {Examples}[https://git.sr.ht/~kerrick/rooibos/tree/trunk/item/examples]
348
+
349
+ [Coming From...]
350
+ {React/Redux}[https://git.sr.ht/~kerrick/rooibos/tree/trunk/item/doc/getting_started/for_react_developers.md],
351
+ {BubbleTea}[https://git.sr.ht/~kerrick/rooibos/tree/trunk/item/doc/getting_started/for_go_developers.md],
352
+ {Textual}[https://git.sr.ht/~kerrick/rooibos/tree/trunk/item/doc/getting_started/for_python_developers.md]
353
+
354
+ [Learn More]
355
+ {Essentials}[https://git.sr.ht/~kerrick/rooibos/tree/trunk/item/doc/essentials/index.md],
356
+ {Scaling Up}[https://git.sr.ht/~kerrick/rooibos/tree/trunk/item/doc/scaling_up/index.md],
357
+ {Best Practices}[https://git.sr.ht/~kerrick/rooibos/tree/trunk/item/doc/best_practices/index.md],
358
+ {Troubleshooting}[https://git.sr.ht/~kerrick/rooibos/tree/trunk/item/doc/troubleshooting/index.md]
359
+
360
+ [Community]
361
+ {Discuss}[https://lists.sr.ht/~kerrick/ratatui_ruby-discuss],
362
+ {Announcements}[https://lists.sr.ht/~kerrick/ratatui_ruby-announce],
363
+ {Bug Tracker}[https://todo.sr.ht/~kerrick/ratatui_ruby],
364
+ {Contribution Guide}[https://man.sr.ht/~kerrick/ratatui_ruby/contributing.md],
365
+ {Code of Conduct}[https://man.sr.ht/~kerrick/ratatui_ruby/code_of_conduct.md]
366
+
367
+
368
+ ---
369
+
370
+ [Website] https://rooibos.run
371
+ [Source] https://git.sr.ht/~kerrick/rooibos
372
+ [RubyGems] https://rubygems.org/gems/rooibos
373
+
374
+ © 2026 Kerrick Long · Library: LGPL-3.0-or-later · Website: CC-BY-NC-ND-4.0 · Snippets: MIT-0
data/REUSE.toml CHANGED
@@ -8,6 +8,11 @@ path = 'Gemfile.lock'
8
8
  SPDX-FileCopyrightText = "2026 Kerrick Long <me@kerricklong.com>"
9
9
  SPDX-License-Identifier = "CC0-1.0"
10
10
 
11
+ [[annotations]]
12
+ path = 'README.rdoc'
13
+ SPDX-FileCopyrightText = "2025 Kerrick Long <me@kerricklong.com>"
14
+ SPDX-License-Identifier = "CC-BY-SA-4.0"
15
+
11
16
  [[annotations]]
12
17
  path = '**/snapshots/*.txt'
13
18
  SPDX-FileCopyrightText = "2026 Kerrick Long <me@kerricklong.com>"
data/Rakefile CHANGED
@@ -13,4 +13,4 @@ RatatuiRuby::Devtools.install!
13
13
  # Import project-specific tasks
14
14
  Dir.glob("tasks/*.rake").each { |r| import r }
15
15
 
16
- task default: %w[lint:fix test lint reuse steep]
16
+ task default: %w[lint:fix install:force test lint reuse steep]