rooibos 0.5.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 (105) hide show
  1. checksums.yaml +7 -0
  2. data/.builds/ruby-3.2.yml +51 -0
  3. data/.builds/ruby-3.3.yml +51 -0
  4. data/.builds/ruby-3.4.yml +51 -0
  5. data/.builds/ruby-4.0.0.yml +51 -0
  6. data/.pre-commit-config.yaml +16 -0
  7. data/.rubocop.yml +8 -0
  8. data/AGENTS.md +108 -0
  9. data/CHANGELOG.md +214 -0
  10. data/LICENSE +304 -0
  11. data/LICENSES/AGPL-3.0-or-later.txt +235 -0
  12. data/LICENSES/CC-BY-SA-4.0.txt +170 -0
  13. data/LICENSES/CC0-1.0.txt +121 -0
  14. data/LICENSES/LGPL-3.0-or-later.txt +304 -0
  15. data/LICENSES/MIT-0.txt +16 -0
  16. data/LICENSES/MIT.txt +18 -0
  17. data/README.md +183 -0
  18. data/REUSE.toml +24 -0
  19. data/Rakefile +16 -0
  20. data/Steepfile +13 -0
  21. data/doc/concepts/application_architecture.md +197 -0
  22. data/doc/concepts/application_testing.md +49 -0
  23. data/doc/concepts/async_work.md +164 -0
  24. data/doc/concepts/commands.md +530 -0
  25. data/doc/concepts/message_processing.md +51 -0
  26. data/doc/contributors/WIP/decomposition_strategies_analysis.md +258 -0
  27. data/doc/contributors/WIP/implementation_plan.md +409 -0
  28. data/doc/contributors/WIP/init_callable_proposal.md +344 -0
  29. data/doc/contributors/WIP/mvu_tea_implementations_research.md +373 -0
  30. data/doc/contributors/WIP/runtime_refactoring_status.md +47 -0
  31. data/doc/contributors/WIP/task.md +36 -0
  32. data/doc/contributors/WIP/v0.4.0_todo.md +468 -0
  33. data/doc/contributors/design/commands_and_outlets.md +214 -0
  34. data/doc/contributors/kit-no-outlet.md +238 -0
  35. data/doc/contributors/priorities.md +38 -0
  36. data/doc/custom.css +22 -0
  37. data/doc/getting_started/quickstart.md +56 -0
  38. data/doc/images/.gitkeep +0 -0
  39. data/doc/images/verify_readme_usage.png +0 -0
  40. data/doc/images/widget_cmd_exec.png +0 -0
  41. data/doc/index.md +25 -0
  42. data/examples/app_fractal_dashboard/README.md +60 -0
  43. data/examples/app_fractal_dashboard/app.rb +63 -0
  44. data/examples/app_fractal_dashboard/dashboard/base.rb +73 -0
  45. data/examples/app_fractal_dashboard/dashboard/update_helpers.rb +86 -0
  46. data/examples/app_fractal_dashboard/dashboard/update_manual.rb +87 -0
  47. data/examples/app_fractal_dashboard/dashboard/update_router.rb +43 -0
  48. data/examples/app_fractal_dashboard/fragments/custom_shell_input.rb +81 -0
  49. data/examples/app_fractal_dashboard/fragments/custom_shell_modal.rb +82 -0
  50. data/examples/app_fractal_dashboard/fragments/custom_shell_output.rb +90 -0
  51. data/examples/app_fractal_dashboard/fragments/disk_usage.rb +47 -0
  52. data/examples/app_fractal_dashboard/fragments/network_panel.rb +45 -0
  53. data/examples/app_fractal_dashboard/fragments/ping.rb +47 -0
  54. data/examples/app_fractal_dashboard/fragments/stats_panel.rb +45 -0
  55. data/examples/app_fractal_dashboard/fragments/system_info.rb +47 -0
  56. data/examples/app_fractal_dashboard/fragments/uptime.rb +47 -0
  57. data/examples/verify_readme_usage/README.md +54 -0
  58. data/examples/verify_readme_usage/app.rb +47 -0
  59. data/examples/widget_command_system/README.md +70 -0
  60. data/examples/widget_command_system/app.rb +132 -0
  61. data/exe/.gitkeep +0 -0
  62. data/lib/rooibos/command/all.rb +69 -0
  63. data/lib/rooibos/command/batch.rb +77 -0
  64. data/lib/rooibos/command/custom.rb +104 -0
  65. data/lib/rooibos/command/http.rb +192 -0
  66. data/lib/rooibos/command/lifecycle.rb +134 -0
  67. data/lib/rooibos/command/outlet.rb +157 -0
  68. data/lib/rooibos/command/wait.rb +80 -0
  69. data/lib/rooibos/command.rb +546 -0
  70. data/lib/rooibos/error.rb +55 -0
  71. data/lib/rooibos/message/all.rb +45 -0
  72. data/lib/rooibos/message/http_response.rb +61 -0
  73. data/lib/rooibos/message/system/batch.rb +61 -0
  74. data/lib/rooibos/message/system/stream.rb +67 -0
  75. data/lib/rooibos/message/timer.rb +46 -0
  76. data/lib/rooibos/message.rb +38 -0
  77. data/lib/rooibos/router.rb +403 -0
  78. data/lib/rooibos/runtime.rb +396 -0
  79. data/lib/rooibos/shortcuts.rb +49 -0
  80. data/lib/rooibos/test_helper.rb +56 -0
  81. data/lib/rooibos/version.rb +12 -0
  82. data/lib/rooibos.rb +121 -0
  83. data/mise.toml +8 -0
  84. data/rbs_collection.lock.yaml +108 -0
  85. data/rbs_collection.yaml +15 -0
  86. data/sig/concurrent.rbs +72 -0
  87. data/sig/examples/verify_readme_usage/app.rbs +19 -0
  88. data/sig/examples/widget_command_system/app.rbs +26 -0
  89. data/sig/open3.rbs +17 -0
  90. data/sig/rooibos/command.rbs +265 -0
  91. data/sig/rooibos/error.rbs +13 -0
  92. data/sig/rooibos/message.rbs +121 -0
  93. data/sig/rooibos/router.rbs +153 -0
  94. data/sig/rooibos/runtime.rbs +75 -0
  95. data/sig/rooibos/shortcuts.rbs +16 -0
  96. data/sig/rooibos/test_helper.rbs +10 -0
  97. data/sig/rooibos/version.rbs +8 -0
  98. data/sig/rooibos.rbs +46 -0
  99. data/tasks/example_viewer.html.erb +172 -0
  100. data/tasks/resources/build.yml.erb +53 -0
  101. data/tasks/resources/index.html.erb +44 -0
  102. data/tasks/resources/rubies.yml +7 -0
  103. data/tasks/steep.rake +11 -0
  104. data/vendor/goodcop/base.yml +1047 -0
  105. metadata +241 -0
@@ -0,0 +1,153 @@
1
+ #--
2
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ # SPDX-License-Identifier: LGPL-3.0-or-later
4
+ #++
5
+
6
+ module Rooibos
7
+ # Declarative DSL for Fractal Architecture.
8
+ module Router
9
+ def self.included: (Class base) -> void
10
+
11
+ # Duck type for Data-like models that support `with`.
12
+ interface _DataModel
13
+ def public_send: (Symbol, *Object) -> Object
14
+ def with: (**Object) -> self
15
+ end
16
+
17
+ # Interface for fractal child modules (fragments).
18
+ # Routed modules must have an UPDATE constant that handles child messages.
19
+ # NOTE: We use Module here because RBS interfaces can't declare class methods.
20
+
21
+ # Configuration for key handlers.
22
+ class KeyHandlerConfig < Data
23
+ attr_reader handler: (^() -> Command::execution?)?
24
+ attr_reader action: Symbol?
25
+ attr_reader route: Symbol?
26
+ attr_reader guard: (^(_DataModel) -> bool)?
27
+
28
+ def initialize: (?handler: (^() -> Command::execution?)?, ?action: Symbol?, ?route: Symbol?, ?guard: (^(_DataModel) -> bool)?) -> void
29
+ end
30
+
31
+ # Configuration for scroll handlers (no coordinates).
32
+ class ScrollHandlerConfig < Data
33
+ attr_reader handler: (^() -> Command::execution?)?
34
+ attr_reader action: Symbol?
35
+
36
+ def initialize: (?handler: (^() -> Command::execution?)?, ?action: Symbol?) -> void
37
+ end
38
+
39
+ # Configuration for click handlers (x, y coordinates).
40
+ class ClickHandlerConfig < Data
41
+ attr_reader handler: (^(Integer, Integer) -> Command::execution?)?
42
+ attr_reader action: Symbol?
43
+
44
+ def initialize: (?handler: (^(Integer, Integer) -> Command::execution?)?, ?action: Symbol?) -> void
45
+ end
46
+
47
+ # Class methods added when Router is included.
48
+ module ClassMethods : Module
49
+ @routes: Hash[Symbol, Module]
50
+ @actions: Hash[Symbol, ^() -> Command::execution?]
51
+ @key_handlers: Hash[String, KeyHandlerConfig]
52
+ @scroll_handlers: Hash[Symbol, ScrollHandlerConfig]
53
+ @click_handler: ClickHandlerConfig?
54
+
55
+ def route: (Symbol | String prefix, to: Module) -> void
56
+ def routes: () -> Hash[Symbol, Module]
57
+
58
+ def action: (Symbol | String name, ^() -> Command::execution? handler) -> void
59
+ def actions: () -> Hash[Symbol, ^() -> Command::execution?]
60
+
61
+ def keymap: () { (KeymapBuilder) [self: KeymapBuilder] -> void } -> void
62
+ def key_handlers: () -> Hash[String, KeyHandlerConfig]
63
+
64
+ def mousemap: () { (MousemapBuilder) [self: MousemapBuilder] -> void } -> void
65
+ def scroll_handlers: () -> Hash[Symbol, ScrollHandlerConfig]
66
+ def click_handler: () -> ClickHandlerConfig?
67
+
68
+ # Returns UPDATE callable that handles routing.
69
+ # Uses is_a? checks for Event::Key and Event::Mouse type narrowing.
70
+ # Model must be Data-like with `with` and field accessors.
71
+ def from_router: () -> RouterUpdate
72
+ end
73
+
74
+ # UPDATE callable returned by from_router with proper typing.
75
+ # Implements #call to satisfy Proc-like interfaces.
76
+ class RouterUpdate
77
+ @routes: Hash[Symbol, Module]
78
+ @actions: Hash[Symbol, ^() -> Command::execution?]
79
+ @key_handlers: Hash[String, KeyHandlerConfig]
80
+ @scroll_handlers: Hash[Symbol, ScrollHandlerConfig]
81
+ @click_handler: ClickHandlerConfig?
82
+
83
+ def initialize: (
84
+ routes: Hash[Symbol, Module],
85
+ actions: Hash[Symbol, ^() -> Command::execution?],
86
+ key_handlers: Hash[String, KeyHandlerConfig],
87
+ scroll_handlers: Hash[Symbol, ScrollHandlerConfig],
88
+ click_handler: ClickHandlerConfig?
89
+ ) -> void
90
+
91
+ # Process message and return [model, command] tuple.
92
+ def call: (
93
+ (RatatuiRuby::Event::Key | RatatuiRuby::Event::Mouse | Array[Object]) message,
94
+ _DataModel model
95
+ ) -> [_DataModel, Command::execution?]
96
+ end
97
+
98
+ # Builder for keymap DSL.
99
+ class KeymapBuilder
100
+ @handlers: Hash[String, KeyHandlerConfig]
101
+ @guard_stack: Array[^(_DataModel) -> bool]
102
+
103
+ attr_reader handlers: Hash[String, KeyHandlerConfig]
104
+
105
+ def initialize: () -> void
106
+
107
+ def key: (
108
+ String | Symbol key_name,
109
+ (^() -> Command::execution?) | Symbol handler_or_action,
110
+ ?route: Symbol?,
111
+ ?when: (^(_DataModel) -> bool)?,
112
+ ?if: (^(_DataModel) -> bool)?,
113
+ ?only: (^(_DataModel) -> bool)?,
114
+ ?guard: (^(_DataModel) -> bool)?,
115
+ ?unless: (^(_DataModel) -> bool)?,
116
+ ?except: (^(_DataModel) -> bool)?,
117
+ ?skip: (^(_DataModel) -> bool)?
118
+ ) -> void
119
+
120
+ def only: (
121
+ ?when: (^(_DataModel) -> bool)?,
122
+ ?if: (^(_DataModel) -> bool)?,
123
+ ?only: (^(_DataModel) -> bool)?,
124
+ ?guard: (^(_DataModel) -> bool)?
125
+ ) { () -> void } -> void
126
+
127
+ def skip: (
128
+ ?when: (^(_DataModel) -> bool)?,
129
+ ?if: (^(_DataModel) -> bool)?,
130
+ ?skip: (^(_DataModel) -> bool)?,
131
+ ?guard: (^(_DataModel) -> bool)?
132
+ ) { () -> void } -> void
133
+
134
+ private
135
+
136
+ def with_guard: ((^(_DataModel) -> bool)?) { () -> void } -> void
137
+ end
138
+
139
+ # Builder for mousemap DSL.
140
+ class MousemapBuilder
141
+ @scroll_handlers: Hash[Symbol, ScrollHandlerConfig]
142
+ @click_handler: ClickHandlerConfig?
143
+
144
+ attr_reader scroll_handlers: Hash[Symbol, ScrollHandlerConfig]
145
+ attr_reader click_handler: ClickHandlerConfig?
146
+
147
+ def initialize: () -> void
148
+
149
+ def click: ((^(Integer, Integer) -> Command::execution?) | Symbol handler_or_action) -> void
150
+ def scroll: (:up | :down direction, (^() -> Command::execution?) | Symbol handler_or_action) -> void
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,75 @@
1
+ #--
2
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ # SPDX-License-Identifier: LGPL-3.0-or-later
4
+ #++
5
+
6
+ module Rooibos
7
+ # MVU runtime event loop.
8
+ class Runtime
9
+ # Active command tracking entry.
10
+ type active_entry = { future: Concurrent::Promises::Future[void], origin: Concurrent::Promises::ResolvableEvent }
11
+
12
+ # Widget type accepted by view functions.
13
+ type renderable = RatatuiRuby::_CustomWidget | RatatuiRuby::widget
14
+
15
+ # Duck type for update result that can be normalized.
16
+ # Steep needs a union type (not interface) for is_a? narrowing.
17
+ type update_result = [Object, Command::execution?] | Command::execution | Object
18
+
19
+ # Duck type for init result that can be normalized.
20
+ type init_result = [Object, Command::execution?] | Command::execution | Object
21
+
22
+ # Duck type for values that can be queried for command-ness.
23
+ interface _MaybeCommand
24
+ def nil?: () -> bool
25
+ def class: () -> Class
26
+ def respond_to?: (Symbol, ?bool) -> bool
27
+ def rooibos_command?: () -> bool
28
+ end
29
+
30
+ # Starts the MVU event loop (positional fragment).
31
+ def self.run: [Model] (
32
+ ?Module? root_fragment,
33
+ ?fps: Integer
34
+ ) -> Model
35
+
36
+ # Starts the MVU event loop (explicit parameters).
37
+ | [Model] (
38
+ ?Module? root_fragment,
39
+ ?fps: Integer,
40
+ model: Model,
41
+ view: ^(Model, RatatuiRuby::TUI) -> renderable,
42
+ update: ^(RatatuiRuby::Event, Model) -> update_result?,
43
+ ?command: Command::execution?
44
+ ) -> Model
45
+
46
+ # Starts the MVU event loop (explicit parameters without fps).
47
+ | [Model] (
48
+ ?Module? root_fragment,
49
+ model: Model,
50
+ view: ^(Model, RatatuiRuby::TUI) -> renderable,
51
+ update: ^(RatatuiRuby::Event, Model) -> update_result?,
52
+ ?command: Command::execution?
53
+ ) -> Model
54
+
55
+ # Normalizes Init callable return value to [model, command] tuple.
56
+ def self.normalize_init: [Model] (init_result result) -> [Model?, Command::execution?]
57
+ QUIT: Object
58
+
59
+ private
60
+
61
+ def self.validate_view_return!: (renderable? widget) -> void
62
+ def self.normalize_update_return: [Model] (update_result? result, Model? previous_model) -> [Model?, Command::execution?]
63
+ def self.validate_ractor_shareable!: [T] (T object, String name) -> void
64
+ def self.fragment_from_kwargs: (Module? root_fragment, ?model: untyped, ?view: untyped, ?update: untyped, ?command: untyped) -> Module
65
+ def self.fragment_invariant!: (String param) -> void
66
+ def self.init_callable: [Model] () -> ^() -> [Model?, Command::execution?]
67
+ def self.start_runtime: () -> untyped
68
+ def self.draw_view: () -> void
69
+ def self.handle_ratatui_event: () -> void
70
+ def self.handle_sync: () -> void
71
+ QUEUE_EMPTY: Object
72
+ def self.send_pending_messages: (?dispatch: bool) -> void
73
+ def self.dispatch_command: () -> void
74
+ end
75
+ end
@@ -0,0 +1,16 @@
1
+ #--
2
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ # SPDX-License-Identifier: LGPL-3.0-or-later
4
+ #++
5
+
6
+ module Rooibos
7
+ # Convenient short aliases for Rooibos APIs.
8
+ module Shortcuts
9
+ # Short alias for Command.
10
+ module Cmd
11
+ def self.exit: () -> Command::Exit
12
+ def self.sh: (String command, Symbol | Class tag) -> Command::System
13
+ def self.map: (Command::execution inner_command) { (Array[untyped]) -> Array[untyped] } -> Command::Mapped
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ #--
2
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ # SPDX-License-Identifier: LGPL-3.0-or-later
4
+ #++
5
+
6
+ module Rooibos
7
+ module TestHelper
8
+ def validate_rooibos_command!: (Runtime::_MaybeCommand) -> nil
9
+ end
10
+ end
@@ -0,0 +1,8 @@
1
+ #--
2
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ # SPDX-License-Identifier: LGPL-3.0-or-later
4
+ #++
5
+
6
+ module Rooibos
7
+ VERSION: String
8
+ end
data/sig/rooibos.rbs ADDED
@@ -0,0 +1,46 @@
1
+ #--
2
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ # SPDX-License-Identifier: LGPL-3.0-or-later
4
+ #++
5
+
6
+ # The Elm Architecture for Ruby.
7
+ module Rooibos
8
+ # Starts the MVU event loop (positional fragment).
9
+ def self.run: [Model] (
10
+ ?Module? root_fragment,
11
+ ?fps: Integer
12
+ ) -> Model
13
+
14
+ # Starts the MVU event loop (explicit parameters).
15
+ | [Model] (
16
+ ?Module? root_fragment,
17
+ ?fps: Integer,
18
+ model: Model,
19
+ view: ^(Model, RatatuiRuby::TUI) -> Runtime::renderable,
20
+ update: ^(RatatuiRuby::Event, Model) -> Runtime::update_result?,
21
+ ?command: Command::execution?
22
+ ) -> Model
23
+
24
+ # Starts the MVU event loop (explicit parameters without fps).
25
+ | [Model] (
26
+ ?Module? root_fragment,
27
+ model: Model,
28
+ view: ^(Model, RatatuiRuby::TUI) -> Runtime::renderable,
29
+ update: ^(RatatuiRuby::Event, Model) -> Runtime::update_result?,
30
+ ?command: Command::execution?
31
+ ) -> Model
32
+
33
+ # Wraps a command with a routing prefix.
34
+ def self.route: (Command::execution command, Symbol prefix) -> Command::Mapped
35
+
36
+ # Delegates a prefixed message to a child fragment's UPDATE.
37
+ def self.delegate: (
38
+ untyped message,
39
+ Symbol prefix,
40
+ ^(Array[untyped]?, untyped) -> [untyped, Command::execution?] child_update,
41
+ untyped child_model
42
+ ) -> ([untyped, Command::execution?] | nil)
43
+
44
+ # Normalizes Init callable return value to [model, command] tuple.
45
+ def self.normalize_init: [Model] (Runtime::init_result result) -> [Model?, Command::execution?]
46
+ end
@@ -0,0 +1,172 @@
1
+ <%#
2
+ SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
3
+ SPDX-License-Identifier: LGPL-3.0-or-later
4
+ %>
5
+ <!DOCTYPE html>
6
+ <html lang="en">
7
+ <head>
8
+ <meta charset="UTF-8">
9
+ <meta name="viewport" content="width=device-width, initial-scale=1">
10
+ <title><%= page_title %> - Example Viewer</title>
11
+
12
+ <script>
13
+ var rdoc_rel_prefix = "<%= doc_root_link.sub('index.html', '') %>";
14
+ var index_rel_prefix = "<%= '../' * (relative_path.split('/').size - 1) %>";
15
+ </script>
16
+
17
+ <script src="<%= '../' * (relative_path.split('/').size - 1) %>js/search_navigation.js" defer></script>
18
+ <script src="<%= '../' * (relative_path.split('/').size - 1) %>js/search_data.js" defer></script>
19
+ <script src="<%= '../' * (relative_path.split('/').size - 1) %>js/search_ranker.js" defer></script>
20
+ <script src="<%= '../' * (relative_path.split('/').size - 1) %>js/search_controller.js" defer></script>
21
+ <script src="<%= '../' * (relative_path.split('/').size - 1) %>js/aliki.js" defer></script>
22
+
23
+ <link href="<%= doc_root_link.sub('index.html', '') %>css/rdoc.css" rel="stylesheet">
24
+ <link href="<%= doc_root_link.sub('index.html', '') %>custom.css" rel="stylesheet">
25
+ </head>
26
+ <body class="file<%= ' has-toc' unless toc_items.empty? %>">
27
+ <%= icons_svg %>
28
+ <header class="top-navbar">
29
+ <div class="navbar-brand">
30
+ Example Viewer
31
+ </div>
32
+
33
+ <!-- Desktop search bar -->
34
+ <div class="navbar-search navbar-search-desktop" role="search">
35
+ <form action="#" method="get" accept-charset="utf-8">
36
+ <input id="search-field" role="combobox" aria-label="Search"
37
+ aria-autocomplete="list" aria-controls="search-results-desktop"
38
+ type="text" name="search" placeholder="Search (/) examples..."
39
+ spellcheck="false" autocomplete="off"
40
+ title="Type to search, Up and Down to navigate, Enter to load">
41
+ <ul id="search-results-desktop" aria-label="Search Results"
42
+ aria-busy="false" aria-expanded="false"
43
+ aria-atomic="false" class="initially-hidden search-results"></ul>
44
+ </form>
45
+ </div>
46
+
47
+ <!-- Mobile search icon button -->
48
+ <button id="search-toggle" class="navbar-search-mobile" aria-label="Open search" type="button">
49
+ <span aria-hidden="true">🔍</span>
50
+ </button>
51
+
52
+ <button id="theme-toggle" class="theme-toggle" aria-label="Switch to dark mode" type="button" onclick="cycleColorMode()">
53
+ <span class="theme-toggle-icon" aria-hidden="true">🌙</span>
54
+ </button>
55
+ </header>
56
+
57
+ <!-- Search Modal (Mobile) -->
58
+ <div id="search-modal" class="search-modal" hidden aria-modal="true" role="dialog" aria-label="Search">
59
+ <div class="search-modal-backdrop"></div>
60
+ <div class="search-modal-content">
61
+ <div class="search-modal-header">
62
+ <form class="search-modal-form" action="#" method="get" accept-charset="utf-8">
63
+ <span class="search-modal-icon" aria-hidden="true">🔍</span>
64
+ <input id="search-field-mobile" role="combobox" aria-label="Search"
65
+ aria-autocomplete="list" aria-controls="search-results-mobile"
66
+ type="text" name="search" placeholder="Search examples"
67
+ spellcheck="false" autocomplete="off">
68
+ <button type="button" class="search-modal-close" aria-label="Close search" id="search-modal-close">
69
+ <span aria-hidden="true">esc</span>
70
+ </button>
71
+ </form>
72
+ </div>
73
+ <div class="search-modal-body">
74
+ <ul id="search-results-mobile" aria-label="Search Results"
75
+ aria-busy="false" aria-expanded="false"
76
+ aria-atomic="false" class="search-results search-modal-results initially-hidden"></ul>
77
+ <div class="search-modal-empty">
78
+ <p>No recent searches</p>
79
+ </div>
80
+ </div>
81
+ </div>
82
+ </div>
83
+
84
+ <nav id="navigation" role="navigation">
85
+ <div id="fileindex-section" class="nav-section">
86
+ <details class="nav-section-collapsible" open>
87
+ <summary class="nav-section-header">
88
+ <span class="nav-section-icon">
89
+ <svg><use href="#icon-file"></use></svg>
90
+ </span>
91
+ <span class="nav-section-title">Examples</span>
92
+ <span class="nav-section-chevron">
93
+ <svg><use href="#icon-chevron"></use></svg>
94
+ </span>
95
+ </summary>
96
+ <ul class="nav-list">
97
+ <li><a href="<%= doc_root_link %>">← Back to Docs</a></li>
98
+ </ul>
99
+ </details>
100
+ </div>
101
+ <div class="nav-section">
102
+ <details class="nav-section-collapsible" open>
103
+ <summary class="nav-section-header">
104
+ <span class="nav-section-icon">
105
+ <svg><use href="#icon-layers"></use></svg>
106
+ </span>
107
+ <span class="nav-section-title">Files</span>
108
+ <span class="nav-section-chevron">
109
+ <svg><use href="#icon-chevron"></use></svg>
110
+ </span>
111
+ </summary>
112
+ <ul class="link-list nav-list">
113
+ <%= render_tree(tree_data, relative_path, current_file_html) %>
114
+ </ul>
115
+ </details>
116
+ </div>
117
+ </nav>
118
+
119
+ <main role="main">
120
+ <div class="breadcrumb">
121
+ <%= breadcrumb_path %>
122
+ </div>
123
+ <%= file_header_html %>
124
+ <div class="content">
125
+ <%= file_content_html %>
126
+ </div>
127
+ </main>
128
+
129
+ <% unless toc_items.empty? %>
130
+ <aside class="table-of-contents" role="complementary" aria-label="Table of Contents">
131
+ <div class="toc-sticky">
132
+ <div class="toc-list">
133
+ <h3>On This Page</h3>
134
+ <%= render_toc(toc_items) %>
135
+ </div>
136
+ </div>
137
+ </aside>
138
+ <% end %>
139
+
140
+
141
+ <script>
142
+ const modes = ['auto', 'light', 'dark'];
143
+ const icons = { auto: '🌓', light: '☀️', dark: '🌙' };
144
+
145
+ function setColorMode(mode) {
146
+ if (mode === 'auto') {
147
+ document.documentElement.removeAttribute('data-theme');
148
+ const systemTheme = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
149
+ document.documentElement.setAttribute('data-theme', systemTheme);
150
+ } else {
151
+ document.documentElement.setAttribute('data-theme', mode);
152
+ }
153
+
154
+ const icon = icons[mode];
155
+ const toggle = document.getElementById('theme-toggle');
156
+ toggle.querySelector('.theme-toggle-icon').textContent = icon;
157
+
158
+ localStorage.setItem('rdoc-theme', mode);
159
+ }
160
+
161
+ function cycleColorMode() {
162
+ const current = localStorage.getItem('rdoc-theme') || 'auto';
163
+ const currentIndex = modes.indexOf(current);
164
+ const nextMode = modes[(currentIndex + 1) % modes.length];
165
+ setColorMode(nextMode);
166
+ }
167
+
168
+ const savedMode = localStorage.getItem('rdoc-theme') || 'auto';
169
+ setColorMode(savedMode);
170
+ </script>
171
+ </body>
172
+ </html>
@@ -0,0 +1,53 @@
1
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
2
+ # SPDX-License-Identifier: LGPL-3.0-or-later
3
+
4
+ image: archlinux
5
+ packages:
6
+ - bash
7
+ - base-devel
8
+ - curl
9
+ - openssl
10
+ - libyaml
11
+ - zlib
12
+ - readline
13
+ - gdbm
14
+ - ncurses
15
+ - libffi
16
+ - clang
17
+ - git
18
+ artifacts:
19
+ - <%= gem_name %>/pkg/<%= gem_filename %>
20
+ sources:
21
+ - https://git.sr.ht/~kerrick/<%= gem_name %>
22
+ tasks:
23
+ - setup: |
24
+ curl https://mise.jdx.dev/install.sh | sh
25
+ echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.buildenv
26
+ echo 'eval "$($HOME/.local/bin/mise activate bash)"' >> ~/.buildenv
27
+ echo 'export LANG="en_US.UTF-8"' >> ~/.buildenv
28
+ echo 'export LC_ALL="en_US.UTF-8"' >> ~/.buildenv
29
+ echo 'export BINDGEN_EXTRA_CLANG_ARGS="-include stdbool.h"' >> ~/.buildenv
30
+ . ~/.buildenv
31
+ export CI="true"
32
+ cd <%= gem_name %>
33
+ sed -i 's/ruby = .*/ruby = "<%= ruby_version %>"/' mise.toml
34
+ mise install
35
+ mise x -- pip install reuse
36
+ mise x -- gem install bundler:<%= bundler_version %>
37
+ mise reshim
38
+ mise x -- bundle config set --local frozen 'true'
39
+ mise x -- bundle install
40
+ - test: |
41
+ . ~/.buildenv
42
+ cd <%= gem_name %>
43
+ echo "Testing Ruby <%= ruby_version %>"
44
+ mise x -- bundle exec rake test
45
+ - lint: |
46
+ . ~/.buildenv
47
+ cd <%= gem_name %>
48
+ echo "Linting Ruby <%= ruby_version %>"
49
+ mise x -- bundle exec rake lint
50
+ - package: |
51
+ . ~/.buildenv
52
+ cd <%= gem_name %>
53
+ mise x -- bundle exec rake build
@@ -0,0 +1,44 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
3
+
4
+ SPDX-License-Identifier: LGPL-3.0-or-later
5
+ -->
6
+
7
+ <!DOCTYPE html>
8
+ <html>
9
+ <head>
10
+ <title><%= project_name %> documentation</title>
11
+ <meta name="viewport" content="width=device-width, initial-scale=1">
12
+ <style>
13
+ :root { color-scheme: light dark; }
14
+ body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 2rem; line-height: 1.5; color: light-dark(#111, #eee); background: light-dark(#fff, #111); }
15
+ h1 { border-bottom: 2px solid light-dark(#eee, #333); padding-bottom: 0.5rem; }
16
+ ul { list-style: none; padding: 0; }
17
+ li { margin: 0.5rem 0; border: 1px solid light-dark(#ddd, #444); border-radius: 4px; }
18
+ a { display: block; padding: 1rem; text-decoration: none; color: light-dark(#0055aa, #44aaff); font-weight: bold; }
19
+ a:hover { background: light-dark(#f5f5f5, #222); }
20
+ .meta { font-weight: normal; color: light-dark(#666, #aaa); font-size: 0.9em; float: right; }
21
+ </style>
22
+ </head>
23
+ <body>
24
+ <h1><%= project_name %> documentation</h1>
25
+ <ul>
26
+ <% versions.each do |version| %>
27
+ <li>
28
+ <a href='<%= version.slug %>/index.html'>
29
+ <%= version.name %>
30
+ <span class='meta'>
31
+ <% if version.latest? %>
32
+ Latest
33
+ <% elsif version.edge? %>
34
+ Edge
35
+ <% else %>
36
+ Historical
37
+ <% end %>
38
+ </span>
39
+ </a>
40
+ </li>
41
+ <% end %>
42
+ </ul>
43
+ </body>
44
+ </html>
@@ -0,0 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ # SPDX-License-Identifier: LGPL-3.0-or-later
3
+
4
+ - "3.2"
5
+ - "3.3"
6
+ - "3.4"
7
+ - "4.0.0"
data/tasks/steep.rake ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: LGPL-3.0-or-later
6
+ #++
7
+
8
+ desc "Run Steep type checker"
9
+ task :steep do
10
+ sh "bundle exec steep check"
11
+ end