rooibos 0.6.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.builds/ruby-3.2.yml +1 -1
- data/.builds/ruby-3.3.yml +1 -1
- data/.builds/ruby-3.4.yml +1 -1
- data/.builds/ruby-4.0.0.yml +1 -1
- data/CHANGELOG.md +37 -0
- data/doc/contributors/maybe_stateful_router.md +56 -0
- data/doc/contributors/specs/file_browser_stories.md +11 -1
- data/examples/{app_file_browser → tutorial/01}/app.rb +14 -4
- data/examples/tutorial/02/app.rb +64 -0
- data/examples/tutorial/03/app.rb +91 -0
- data/examples/tutorial/06_safe_refactoring/app.rb +124 -0
- data/lib/rooibos/message/routed.rb +51 -0
- data/lib/rooibos/message.rb +1 -0
- data/lib/rooibos/router.rb +148 -20
- data/lib/rooibos/version.rb +1 -1
- data/sig/rooibos/message.rbs +17 -0
- data/sig/rooibos/router.rbs +30 -9
- metadata +9 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f0bd248d3b6933a881c6a2dc2e01eff42ecf20ae9b8547237f216de1ba3fcc54
|
|
4
|
+
data.tar.gz: ef8473cff78d86f01022ae554b9a60bf7fdadbfd8528d499cd59ccc592ce5a2a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e3dbe609e9423b3e7e9335c72da14e3eca798d4083baf6f7c17761eeb7f42267bd68d391e6e0102d225f9c3f7e4fc1799221547440baa2d5f9c28beed96ac5b8
|
|
7
|
+
data.tar.gz: 49d737453d32dc06193baf1b529270938ae8861afcf9f2e5c1faef0a6a04b594d2cd8d63dfb4892c51b2b64b5e06c52c3dbbb260daaa7184a961d0969ca26b39
|
data/.builds/ruby-3.2.yml
CHANGED
data/.builds/ruby-3.3.yml
CHANGED
data/.builds/ruby-3.4.yml
CHANGED
data/.builds/ruby-4.0.0.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -21,6 +21,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
21
21
|
|
|
22
22
|
### Removed
|
|
23
23
|
|
|
24
|
+
## [0.6.2] - 2026-01-27
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **Router `action` DSL Enhancements**: Multiple syntax forms for declaring actions with inline keybindings:
|
|
29
|
+
- Positional: `action :quit, -> { Command.exit }` (original)
|
|
30
|
+
- Keyword: `action quit: -> { Command.exit }` (cleaner)
|
|
31
|
+
- Inline keymap: `action quit: -> { Command.exit }, keymap: %i[ctrl_c q]`
|
|
32
|
+
- `key:` singular alias: `action go_home: FileList, key: :~`
|
|
33
|
+
- `keys:` plural alias: `action move_down: FileList, keys: %i[down j]`
|
|
34
|
+
- Anonymous: `action -> { Command.exit }, keymap: %i[ctrl_c q]` (no name, just binding)
|
|
35
|
+
- `mousemap:` for scroll bindings: `action scroll_up: ..., mousemap: %i[scroll_up]`
|
|
36
|
+
|
|
37
|
+
- **Routed Actions**: Actions can now target child fragments. When the value is a `Module`, the Router synthesizes a `Message::Routed` and dispatches to the child's Update:
|
|
38
|
+
- `action move_down: FileList` — declares `FileList` as target
|
|
39
|
+
- Key presses trigger `Message::Routed.new(envelope: :move_down, event: key_event)`
|
|
40
|
+
|
|
41
|
+
- **Message::Routed**: New message type for routed actions between parent and child fragments:
|
|
42
|
+
- `deconstruct_keys` returns `{ type: :routed, envelope:, event: }`
|
|
43
|
+
- Predicate methods via `method_missing`: `msg.move_down?`, `msg.go_back?`
|
|
44
|
+
- Carries original event for full context in child Update
|
|
45
|
+
|
|
46
|
+
- **Keymap `key` DSL Enhancements**: Multiple syntax forms for declaring key handlers:
|
|
47
|
+
- Variadic keys: `key :down, :j, action: :move_down` (multiple keys, one action)
|
|
48
|
+
- `action:` keyword: `key :q, action: :quit` (explicit action reference)
|
|
49
|
+
- Keyword syntax: `key q: -> { Command.exit }` (hash-style)
|
|
50
|
+
- Multi-keyword: `keys ctrl_c: -> { ... }, q: -> { ... }` (multiple bindings)
|
|
51
|
+
- Hash metaprogramming: `key(exit_bindings)` where `exit_bindings = { q: ..., esc: ... }`
|
|
52
|
+
- `keys` alias: `alias_method :keys, :key` for readability
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
### Removed
|
|
59
|
+
|
|
24
60
|
## [0.6.1] - 2026-01-26
|
|
25
61
|
|
|
26
62
|
### Added
|
|
@@ -260,6 +296,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
260
296
|
- **First Release**: Empty release of `rooibos`, a Ruby implementation of The Elm Architecture (TEA) for `ratatui_ruby`. Scaffolding generated by `ratatui_ruby-devtools`.
|
|
261
297
|
|
|
262
298
|
[Unreleased]: https://git.sr.ht/~kerrick/rooibos/refs/HEAD
|
|
299
|
+
[0.6.2]: https://git.sr.ht/~kerrick/rooibos/refs/v0.6.2
|
|
263
300
|
[0.6.1]: https://git.sr.ht/~kerrick/rooibos/refs/v0.6.1
|
|
264
301
|
[0.6.0]: https://git.sr.ht/~kerrick/rooibos/refs/v0.6.0
|
|
265
302
|
[0.5.0]: https://git.sr.ht/~kerrick/rooibos/refs/v0.5.0
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
# Maybe: Stateful Router
|
|
7
|
+
|
|
8
|
+
**Status:** Not implemented — noted for future consideration.
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Current routed actions require the parent fragment's model to hold child fragments' models (e.g., `model.file_list`). The Router then needs to know which field name to use when dispatching to children and updating the parent model.
|
|
13
|
+
|
|
14
|
+
This couples the Router to the parent's model structure.
|
|
15
|
+
|
|
16
|
+
## Proposed Alternative
|
|
17
|
+
|
|
18
|
+
Make the Router stateful, holding child fragment models internally:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
module ClassMethods
|
|
22
|
+
def route(prefix, to:)
|
|
23
|
+
# Router would also initialize and store child model
|
|
24
|
+
child_model, init_command = to.const_get(:Init).()
|
|
25
|
+
routed_models[prefix] = child_model
|
|
26
|
+
routes[prefix] = to
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Pattern Symmetry
|
|
32
|
+
|
|
33
|
+
This mirrors the architecture at every level:
|
|
34
|
+
|
|
35
|
+
| Component | Holds Model | Calls Update |
|
|
36
|
+
|-----------|-------------|--------------|
|
|
37
|
+
| Runtime | Root model | Root Update |
|
|
38
|
+
| Router | Child models| Child Updates|
|
|
39
|
+
|
|
40
|
+
## Pros
|
|
41
|
+
|
|
42
|
+
- Parent fragment doesn't need nested model composition
|
|
43
|
+
- Router becomes a true "mini-runtime" for children
|
|
44
|
+
- Enables features like broadcasting resize events to all children
|
|
45
|
+
- Parent's Update is simpler — just handles its own concerns
|
|
46
|
+
|
|
47
|
+
## Cons
|
|
48
|
+
|
|
49
|
+
- Breaks from strict Elm pattern where parent Model contains child Models
|
|
50
|
+
- Router becomes stateful (but Runtime already is)
|
|
51
|
+
- Initialization order requires careful thought
|
|
52
|
+
- View needs a different way to access child models for rendering
|
|
53
|
+
|
|
54
|
+
## Decision
|
|
55
|
+
|
|
56
|
+
Deferred. Current implementation requires parent to hold child models, which follows canonical Elm patterns. The stateful router approach is coherent but would be a significant architectural change.
|
|
@@ -133,6 +133,8 @@ _Note: If you started with negative-numbered stories, this happened in Story 0._
|
|
|
133
133
|
- Selected item is visually highlighted
|
|
134
134
|
- Selection wraps at top/bottom of list
|
|
135
135
|
- `j`/`k` vim keys also work
|
|
136
|
+
- `Home`/`g` jumps to first item
|
|
137
|
+
- `End`/`G` jumps to last item
|
|
136
138
|
|
|
137
139
|
### Notes
|
|
138
140
|
- Introduces state management (current selection index)
|
|
@@ -152,7 +154,11 @@ _Note: If you started with negative-numbered stories, this happened in Story 0._
|
|
|
152
154
|
- File list updates to show new directory contents
|
|
153
155
|
- Current path display updates
|
|
154
156
|
- Enter on regular file does nothing (for now)
|
|
155
|
-
- Backspace goes to parent directory
|
|
157
|
+
- Backspace or `←`/`h` goes to parent directory
|
|
158
|
+
- `→`/`l` enters directory (same as Enter)
|
|
159
|
+
- `~` jumps to home directory
|
|
160
|
+
- `/` jumps to root directory
|
|
161
|
+
- `R` refreshes current view
|
|
156
162
|
|
|
157
163
|
### Notes
|
|
158
164
|
- Introduces directory traversal
|
|
@@ -215,6 +221,8 @@ _Note: If you started with negative-numbered stories, this happened in Story 0._
|
|
|
215
221
|
- Binary files show "Binary file" message
|
|
216
222
|
- Preview pane scrolls if content is long
|
|
217
223
|
- File type is detected (text vs binary)
|
|
224
|
+
- `Space` toggles preview pane visibility
|
|
225
|
+
- `PgUp`/`PgDn` pages through preview content (when focused)
|
|
218
226
|
|
|
219
227
|
### Notes
|
|
220
228
|
- Introduces file reading
|
|
@@ -256,10 +264,12 @@ _Note: If you started with negative-numbered stories, this happened in Story 0._
|
|
|
256
264
|
- Active pane has visual indicator (highlighted border)
|
|
257
265
|
- Keyboard shortcuts work in context of focused pane
|
|
258
266
|
- Arrow keys navigate within focused pane
|
|
267
|
+
- `PgUp`/`PgDn` pages by visible height
|
|
259
268
|
|
|
260
269
|
### Notes
|
|
261
270
|
- Introduces focus management
|
|
262
271
|
- Introduces context-sensitive key handling
|
|
272
|
+
- Introduces cached layout pattern (store pane dimensions for dynamic paging)
|
|
263
273
|
- Preview pane becomes interactive (scrollable)
|
|
264
274
|
|
|
265
275
|
---
|
|
@@ -10,12 +10,15 @@ require "rooibos"
|
|
|
10
10
|
|
|
11
11
|
module Tutorial01
|
|
12
12
|
module FileBrowser
|
|
13
|
-
|
|
13
|
+
Entry = Data.define(:name, :directory?)
|
|
14
|
+
|
|
15
|
+
Model = Data.define(:current_directory, :entries)
|
|
14
16
|
|
|
15
17
|
View = -> (model, tui) {
|
|
18
|
+
items = model.entries.map { |e| e.directory? ? "#{e.name}/" : e.name }
|
|
16
19
|
tui.layout(children: [
|
|
17
20
|
tui.paragraph(text: model.current_directory),
|
|
18
|
-
tui.list(items:
|
|
21
|
+
tui.list(items:),
|
|
19
22
|
])
|
|
20
23
|
}
|
|
21
24
|
|
|
@@ -27,10 +30,17 @@ module Tutorial01
|
|
|
27
30
|
end
|
|
28
31
|
}
|
|
29
32
|
|
|
33
|
+
ReadEntries = -> (path) {
|
|
34
|
+
Dir.children(path).map { |name|
|
|
35
|
+
full_path = File.join(path, name)
|
|
36
|
+
Entry.new(name:, directory?: File.directory?(full_path))
|
|
37
|
+
}.sort_by { |e| [e.directory? ? 0 : 1, e.name.downcase] }
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
Init = -> {
|
|
31
41
|
current_directory = Dir.pwd
|
|
32
|
-
|
|
33
|
-
Ractor.make_shareable Model.new(current_directory,
|
|
42
|
+
entries = ReadEntries.call(current_directory)
|
|
43
|
+
Ractor.make_shareable Model.new(current_directory, entries)
|
|
34
44
|
}
|
|
35
45
|
end
|
|
36
46
|
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
#++
|
|
8
|
+
|
|
9
|
+
require "rooibos"
|
|
10
|
+
|
|
11
|
+
module Tutorial02
|
|
12
|
+
module FileBrowser
|
|
13
|
+
Entry = Data.define(:name, :directory?)
|
|
14
|
+
|
|
15
|
+
Model = Data.define(:current_directory, :entries, :selected_index)
|
|
16
|
+
|
|
17
|
+
View = -> (model, tui) {
|
|
18
|
+
items = model.entries.map { |e| e.directory? ? "#{e.name}/" : e.name }
|
|
19
|
+
tui.layout(children: [
|
|
20
|
+
tui.paragraph(text: model.current_directory),
|
|
21
|
+
tui.list(
|
|
22
|
+
items:,
|
|
23
|
+
selected_index: model.selected_index,
|
|
24
|
+
highlight_style: tui.style(modifiers: [:reversed])
|
|
25
|
+
),
|
|
26
|
+
])
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
Update = -> (message, model) {
|
|
30
|
+
if message.ctrl_c? or message.q?
|
|
31
|
+
Rooibos::Command.exit
|
|
32
|
+
elsif message.down_arrow? or message.j?
|
|
33
|
+
new_index = (model.selected_index + 1) % model.entries.length
|
|
34
|
+
model.with(selected_index: new_index)
|
|
35
|
+
elsif message.up_arrow? or message.k?
|
|
36
|
+
new_index = (model.selected_index - 1) % model.entries.length
|
|
37
|
+
model.with(selected_index: new_index)
|
|
38
|
+
elsif message.home? or message.g?
|
|
39
|
+
model.with(selected_index: 0)
|
|
40
|
+
elsif message.end? or message.G?
|
|
41
|
+
model.with(selected_index: model.entries.length - 1)
|
|
42
|
+
else
|
|
43
|
+
model
|
|
44
|
+
end
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
ReadEntries = -> (path) {
|
|
48
|
+
Dir.children(path).map { |name|
|
|
49
|
+
full_path = File.join(path, name)
|
|
50
|
+
Entry.new(name:, directory?: File.directory?(full_path))
|
|
51
|
+
}.sort_by { |e| [e.directory? ? 0 : 1, e.name.downcase] }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Init = -> {
|
|
55
|
+
current_directory = Dir.pwd
|
|
56
|
+
entries = ReadEntries.call(current_directory)
|
|
57
|
+
Ractor.make_shareable Model.new(current_directory, entries, 0)
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
if __FILE__ == $0
|
|
63
|
+
Rooibos.run(Tutorial02::FileBrowser)
|
|
64
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
#++
|
|
8
|
+
|
|
9
|
+
require "rooibos"
|
|
10
|
+
|
|
11
|
+
module Tutorial03
|
|
12
|
+
module FileBrowser
|
|
13
|
+
Entry = Data.define(:name, :directory?)
|
|
14
|
+
|
|
15
|
+
Model = Data.define(:current_directory, :entries, :selected_index)
|
|
16
|
+
|
|
17
|
+
View = -> (model, tui) {
|
|
18
|
+
items = model.entries.map { |e| e.directory? ? "#{e.name}/" : e.name }
|
|
19
|
+
tui.layout(children: [
|
|
20
|
+
tui.paragraph(text: model.current_directory),
|
|
21
|
+
tui.list(
|
|
22
|
+
items:,
|
|
23
|
+
selected_index: model.selected_index,
|
|
24
|
+
highlight_style: tui.style(modifiers: [:reversed])
|
|
25
|
+
),
|
|
26
|
+
])
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
Update = -> (message, model) {
|
|
30
|
+
if message.ctrl_c? or message.q?
|
|
31
|
+
Rooibos::Command.exit
|
|
32
|
+
elsif message.down_arrow? or message.j?
|
|
33
|
+
new_index = (model.selected_index + 1) % model.entries.length
|
|
34
|
+
model.with(selected_index: new_index)
|
|
35
|
+
elsif message.up_arrow? or message.k?
|
|
36
|
+
new_index = (model.selected_index - 1) % model.entries.length
|
|
37
|
+
model.with(selected_index: new_index)
|
|
38
|
+
elsif message.home? or message.g?
|
|
39
|
+
model.with(selected_index: 0)
|
|
40
|
+
elsif message.end? or message.G?
|
|
41
|
+
model.with(selected_index: model.entries.length - 1)
|
|
42
|
+
elsif message.enter? or message.right_arrow? or message.l?
|
|
43
|
+
selected_entry = model.entries[model.selected_index]
|
|
44
|
+
if selected_entry.directory?
|
|
45
|
+
new_dir = File.join(model.current_directory, selected_entry.name)
|
|
46
|
+
new_entries = read_entries(new_dir)
|
|
47
|
+
model.with(current_directory: new_dir, entries: new_entries, selected_index: 0)
|
|
48
|
+
else
|
|
49
|
+
model
|
|
50
|
+
end
|
|
51
|
+
elsif message.backspace? or message.left_arrow? or message.h?
|
|
52
|
+
parent_dir = File.dirname(model.current_directory)
|
|
53
|
+
new_entries = read_entries(parent_dir)
|
|
54
|
+
model.with(current_directory: parent_dir, entries: new_entries, selected_index: 0)
|
|
55
|
+
elsif message.tilde?
|
|
56
|
+
home_dir = Dir.home
|
|
57
|
+
new_entries = read_entries(home_dir)
|
|
58
|
+
model.with(current_directory: home_dir, entries: new_entries, selected_index: 0)
|
|
59
|
+
elsif message.slash?
|
|
60
|
+
new_entries = read_entries("/")
|
|
61
|
+
model.with(current_directory: "/", entries: new_entries, selected_index: 0)
|
|
62
|
+
elsif message.R?
|
|
63
|
+
new_entries = read_entries(model.current_directory)
|
|
64
|
+
model.with(entries: new_entries)
|
|
65
|
+
else
|
|
66
|
+
model
|
|
67
|
+
end
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
ReadEntries = -> (path) {
|
|
71
|
+
Dir.children(path).map { |name|
|
|
72
|
+
full_path = File.join(path, name)
|
|
73
|
+
Entry.new(name:, directory?: File.directory?(full_path))
|
|
74
|
+
}.sort_by { |e| [e.directory? ? 0 : 1, e.name.downcase] }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
Init = -> {
|
|
78
|
+
current_directory = Dir.pwd
|
|
79
|
+
entries = ReadEntries.call(current_directory)
|
|
80
|
+
Ractor.make_shareable Model.new(current_directory, entries, 0)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private_class_method def self.read_entries(path)
|
|
84
|
+
ReadEntries.call(path)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
if __FILE__ == $0
|
|
90
|
+
Rooibos.run(Tutorial03::FileBrowser)
|
|
91
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
#++
|
|
8
|
+
|
|
9
|
+
require "rooibos"
|
|
10
|
+
|
|
11
|
+
module Tutorial06
|
|
12
|
+
# A Fragment is a module with Model, Init, Update, View.
|
|
13
|
+
# This FileList fragment handles the list of files and navigation.
|
|
14
|
+
module FileList
|
|
15
|
+
Entry = Data.define(:name, :directory?)
|
|
16
|
+
|
|
17
|
+
Model = Data.define(:current_directory, :entries, :selected_index)
|
|
18
|
+
|
|
19
|
+
View = -> (model, tui) {
|
|
20
|
+
items = model.entries.map { |e| e.directory? ? "#{e.name}/" : e.name }
|
|
21
|
+
tui.layout(children: [
|
|
22
|
+
tui.paragraph(text: model.current_directory),
|
|
23
|
+
tui.list(
|
|
24
|
+
items:,
|
|
25
|
+
selected_index: model.selected_index,
|
|
26
|
+
highlight_style: tui.style(modifiers: [:reversed])
|
|
27
|
+
),
|
|
28
|
+
])
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
Update = -> (message, model) {
|
|
32
|
+
if message.move_down?
|
|
33
|
+
new_index = (model.selected_index + 1) % model.entries.length
|
|
34
|
+
model.with(selected_index: new_index)
|
|
35
|
+
elsif message.move_up?
|
|
36
|
+
new_index = (model.selected_index - 1) % model.entries.length
|
|
37
|
+
model.with(selected_index: new_index)
|
|
38
|
+
elsif message.jump_to_first?
|
|
39
|
+
model.with(selected_index: 0)
|
|
40
|
+
elsif message.jump_to_last?
|
|
41
|
+
model.with(selected_index: model.entries.length - 1)
|
|
42
|
+
elsif message.enter?
|
|
43
|
+
selected_entry = model.entries[model.selected_index]
|
|
44
|
+
if selected_entry.directory?
|
|
45
|
+
new_dir = File.join(model.current_directory, selected_entry.name)
|
|
46
|
+
new_entries = read_entries(new_dir)
|
|
47
|
+
model.with(current_directory: new_dir, entries: new_entries, selected_index: 0)
|
|
48
|
+
else
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
elsif message.go_back?
|
|
52
|
+
parent_dir = File.dirname(model.current_directory)
|
|
53
|
+
new_entries = read_entries(parent_dir)
|
|
54
|
+
model.with(current_directory: parent_dir, entries: new_entries, selected_index: 0)
|
|
55
|
+
elsif message.go_home?
|
|
56
|
+
home_dir = Dir.home
|
|
57
|
+
new_entries = read_entries(home_dir)
|
|
58
|
+
model.with(current_directory: home_dir, entries: new_entries, selected_index: 0)
|
|
59
|
+
elsif message.go_root?
|
|
60
|
+
root = File.expand_path("/")
|
|
61
|
+
new_entries = read_entries(root)
|
|
62
|
+
model.with(current_directory: root, entries: new_entries, selected_index: 0)
|
|
63
|
+
elsif message.refresh?
|
|
64
|
+
new_entries = read_entries(model.current_directory)
|
|
65
|
+
model.with(entries: new_entries)
|
|
66
|
+
end
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
ReadEntries = -> (path) {
|
|
70
|
+
Dir.children(path).map { |name|
|
|
71
|
+
full_path = File.join(path, name)
|
|
72
|
+
Entry.new(name:, directory?: File.directory?(full_path))
|
|
73
|
+
}.sort_by { |e| [e.directory? ? 0 : 1, e.name.downcase] }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
Init = -> {
|
|
77
|
+
current_directory = Dir.pwd
|
|
78
|
+
entries = ReadEntries.call(current_directory)
|
|
79
|
+
Ractor.make_shareable Model.new(current_directory, entries, 0)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private_class_method def self.read_entries(path)
|
|
83
|
+
ReadEntries.call(path)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The main FileBrowser delegates to the FileList fragment via Router.
|
|
88
|
+
module FileBrowser
|
|
89
|
+
include Rooibos::Router
|
|
90
|
+
|
|
91
|
+
Command = Rooibos::Command
|
|
92
|
+
|
|
93
|
+
# Parent model wraps child fragment
|
|
94
|
+
Model = Data.define(:file_list)
|
|
95
|
+
|
|
96
|
+
route :file_list, to: FileList
|
|
97
|
+
|
|
98
|
+
action go_back: FileList, keys: %i[backspace left h]
|
|
99
|
+
action go_home: FileList, key: :~
|
|
100
|
+
action go_root: FileList, key: :/
|
|
101
|
+
action refresh: FileList, key: :R
|
|
102
|
+
action enter: FileList, keys: %i[enter right l]
|
|
103
|
+
action move_down: FileList, keys: %i[down j]
|
|
104
|
+
action move_up: FileList, keys: %i[up k]
|
|
105
|
+
action jump_to_first: FileList, keys: %i[home g]
|
|
106
|
+
action jump_to_last: FileList, keys: %i[end G]
|
|
107
|
+
action -> { Command.exit }, keys: %i[ctrl_c q]
|
|
108
|
+
|
|
109
|
+
View = -> (model, tui) {
|
|
110
|
+
FileList::View.call(model.file_list, tui)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
Update = from_router
|
|
114
|
+
|
|
115
|
+
Init = -> {
|
|
116
|
+
file_list = FileList::Init.()
|
|
117
|
+
Ractor.make_shareable Model.new(file_list:)
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
if __FILE__ == $0
|
|
123
|
+
Rooibos.run(Tutorial06::FileBrowser)
|
|
124
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
module Rooibos
|
|
9
|
+
module Message
|
|
10
|
+
# Message synthesized by Router when keymap uses symbol + route.
|
|
11
|
+
#
|
|
12
|
+
# When a keymap entry uses a symbol instead of a handler, and specifies
|
|
13
|
+
# a route, the Router creates this message and dispatches it to the
|
|
14
|
+
# child fragment's UPDATE.
|
|
15
|
+
#
|
|
16
|
+
# === Example
|
|
17
|
+
#
|
|
18
|
+
# # In parent keymap:
|
|
19
|
+
# key :down, :move_down, route: :file_list
|
|
20
|
+
#
|
|
21
|
+
# # Router synthesizes:
|
|
22
|
+
# Routed.new(envelope: :move_down, event: key_event)
|
|
23
|
+
#
|
|
24
|
+
# # Child UPDATE matches:
|
|
25
|
+
# in { type: :routed, envelope: :move_down }
|
|
26
|
+
# handle_move_down(model)
|
|
27
|
+
Routed = Data.define(:envelope, :event) do
|
|
28
|
+
include Predicates
|
|
29
|
+
|
|
30
|
+
def deconstruct_keys(_keys)
|
|
31
|
+
{ type: :routed, envelope:, event: }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def routed?
|
|
35
|
+
true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
39
|
+
method_name.end_with?("?") || super
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def method_missing(method_name, *args, &block)
|
|
43
|
+
if method_name.end_with?("?")
|
|
44
|
+
method_name.to_s.chomp("?") == envelope.to_s
|
|
45
|
+
else
|
|
46
|
+
super
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/rooibos/message.rb
CHANGED
data/lib/rooibos/router.rb
CHANGED
|
@@ -65,12 +65,13 @@ module Rooibos
|
|
|
65
65
|
|
|
66
66
|
# Class methods added when Router is included.
|
|
67
67
|
module ClassMethods
|
|
68
|
-
# Declares a route to a child.
|
|
68
|
+
# Declares a route to a child fragment.
|
|
69
69
|
#
|
|
70
|
-
# [
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
# [fragment_model_instance_attr] Symbol naming the attr on the parent's model
|
|
71
|
+
# that holds this fragment's model instance (normalized via +.to_s.to_sym+).
|
|
72
|
+
# [to] The child fragment module (must have Update and Init constants).
|
|
73
|
+
def route(fragment_model_instance_attr, to:)
|
|
74
|
+
routes[fragment_model_instance_attr.to_s.to_sym] = to
|
|
74
75
|
end
|
|
75
76
|
|
|
76
77
|
# Returns the registered routes hash.
|
|
@@ -84,17 +85,79 @@ module Rooibos
|
|
|
84
85
|
# This avoids duplicating logic for keys and mouse events that do
|
|
85
86
|
# the same thing.
|
|
86
87
|
#
|
|
88
|
+
# Supports both positional and keyword syntax:
|
|
89
|
+
# action :scroll_up, -> { Command.scroll(-1) } # Positional
|
|
90
|
+
# action scroll_up: -> { Command.scroll(-1) } # Keyword
|
|
91
|
+
#
|
|
87
92
|
# [name] Symbol or String identifying the action (normalized via +.to_s.to_sym+).
|
|
88
|
-
# [
|
|
89
|
-
def action(name,
|
|
90
|
-
|
|
93
|
+
# [value] Callable that returns a command or message.
|
|
94
|
+
def action(name = nil, value = nil, keymap: nil, key: nil, keys: nil, mousemap: nil, **kwargs)
|
|
95
|
+
# key: and keys: are aliases for keymap:
|
|
96
|
+
effective_keymap = keymap || key || keys
|
|
97
|
+
action_name, action_value = if name && value
|
|
98
|
+
# Positional: action :name, handler
|
|
99
|
+
[name, value]
|
|
100
|
+
elsif name.respond_to?(:call) && value.nil?
|
|
101
|
+
# Anonymous: action -> { ... }, keymap: %i[...]
|
|
102
|
+
# No name, just handler with bindings
|
|
103
|
+
[nil, name]
|
|
104
|
+
elsif kwargs.size == 1
|
|
105
|
+
# Keyword: action name: handler
|
|
106
|
+
kwargs.first
|
|
107
|
+
else
|
|
108
|
+
raise ArgumentError, "action requires (name, value) or (name: value)"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @type var action_name: Symbol?
|
|
112
|
+
# @type var action_value: (^() -> Command::execution? | Module)?
|
|
113
|
+
register_action(action_name, action_value) if action_name && action_value
|
|
114
|
+
|
|
115
|
+
# For anonymous actions, store handler directly in keymap
|
|
116
|
+
handler_for_keymap = action_name.nil? ? action_value : nil
|
|
117
|
+
|
|
118
|
+
# Register keymap bindings if provided
|
|
119
|
+
if effective_keymap
|
|
120
|
+
Array(effective_keymap).each do |key_name|
|
|
121
|
+
key_handlers[key_name.to_s.to_sym] = Router::KeyHandlerConfig.new(
|
|
122
|
+
handler: handler_for_keymap,
|
|
123
|
+
action: action_name&.to_s&.to_sym,
|
|
124
|
+
guard: nil,
|
|
125
|
+
route: nil
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Register mousemap bindings if provided
|
|
131
|
+
if mousemap
|
|
132
|
+
Array(mousemap).each do |mouse_event|
|
|
133
|
+
scroll_handlers[mouse_event.to_s.to_sym] = Router::ScrollHandlerConfig.new(
|
|
134
|
+
handler: handler_for_keymap,
|
|
135
|
+
action: action_name&.to_s&.to_sym
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
91
139
|
end
|
|
92
140
|
|
|
93
|
-
|
|
141
|
+
private def register_action(name, value)
|
|
142
|
+
key = name.to_s.to_sym
|
|
143
|
+
case value
|
|
144
|
+
when Module
|
|
145
|
+
routed_actions[key] = value
|
|
146
|
+
else
|
|
147
|
+
actions[key] = value
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Returns the registered handler actions hash.
|
|
94
152
|
def actions
|
|
95
153
|
@actions ||= {}
|
|
96
154
|
end
|
|
97
155
|
|
|
156
|
+
# Returns the registered routed actions hash.
|
|
157
|
+
def routed_actions
|
|
158
|
+
@routed_actions ||= {}
|
|
159
|
+
end
|
|
160
|
+
|
|
98
161
|
# Declares key handlers in a block.
|
|
99
162
|
#
|
|
100
163
|
# === Example
|
|
@@ -150,6 +213,7 @@ module Rooibos
|
|
|
150
213
|
RouterUpdate.new(
|
|
151
214
|
routes:,
|
|
152
215
|
actions:,
|
|
216
|
+
routed_actions:,
|
|
153
217
|
key_handlers:,
|
|
154
218
|
scroll_handlers:,
|
|
155
219
|
click_handler:
|
|
@@ -159,9 +223,10 @@ module Rooibos
|
|
|
159
223
|
|
|
160
224
|
# Internal UPDATE callable with proper typing.
|
|
161
225
|
class RouterUpdate # :nodoc:
|
|
162
|
-
def initialize(routes:, actions:, key_handlers:, scroll_handlers:, click_handler:)
|
|
226
|
+
def initialize(routes:, actions:, routed_actions:, key_handlers:, scroll_handlers:, click_handler:)
|
|
163
227
|
@routes = routes
|
|
164
228
|
@actions = actions
|
|
229
|
+
@routed_actions = routed_actions
|
|
165
230
|
@key_handlers = key_handlers
|
|
166
231
|
@scroll_handlers = scroll_handlers
|
|
167
232
|
@click_handler = click_handler
|
|
@@ -195,6 +260,24 @@ module Rooibos
|
|
|
195
260
|
if handler.nil? && config.action
|
|
196
261
|
handler = @actions[config.action]
|
|
197
262
|
end
|
|
263
|
+
|
|
264
|
+
# Check for routed action if no handler found
|
|
265
|
+
if handler.nil? && config.action
|
|
266
|
+
routed_fragment = @routed_actions[config.action]
|
|
267
|
+
if routed_fragment
|
|
268
|
+
# Find the model attr for this fragment
|
|
269
|
+
fragment_model_instance_attr = @routes.key(routed_fragment)
|
|
270
|
+
next unless fragment_model_instance_attr
|
|
271
|
+
|
|
272
|
+
# Synthesize Message::Routed and dispatch to child
|
|
273
|
+
routed_message = Rooibos::Message::Routed.new(envelope: config.action, event: message)
|
|
274
|
+
child_update = routed_fragment.const_get(:Update)
|
|
275
|
+
previous_child_fragment_model_instance = model.public_send(fragment_model_instance_attr)
|
|
276
|
+
updated_child_fragment_model_instance, command = child_update.call(routed_message, previous_child_fragment_model_instance)
|
|
277
|
+
return [model.with(fragment_model_instance_attr => updated_child_fragment_model_instance), command]
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
198
281
|
next unless handler
|
|
199
282
|
|
|
200
283
|
command = handler.call
|
|
@@ -260,18 +343,53 @@ module Rooibos
|
|
|
260
343
|
|
|
261
344
|
# Registers a key handler.
|
|
262
345
|
#
|
|
263
|
-
#
|
|
264
|
-
#
|
|
346
|
+
# Supports multiple forms:
|
|
347
|
+
# key :q, -> { Command.exit } # Single key with handler
|
|
348
|
+
# key :q, :quit # Single key with action name
|
|
349
|
+
# key :down, :j, action: :move_down # Multiple keys with action
|
|
350
|
+
# key :enter, -> { ... }, route: :foo # With options
|
|
351
|
+
#
|
|
352
|
+
# [*key_names] One or more key names (String or Symbol).
|
|
353
|
+
# [handler_or_action] Callable or Symbol (action name) - optional if action: given.
|
|
354
|
+
# [action] Action name as keyword arg (alternative to positional).
|
|
265
355
|
# [route] Optional route prefix for the command result.
|
|
266
356
|
# [when/if/only/guard] Guard that runs if truthy (aliases).
|
|
267
357
|
# [unless/except/skip] Guard that runs if falsy (negative aliases).
|
|
268
|
-
def key(
|
|
358
|
+
def key(*args, action: nil, route: nil, when: nil, if: nil, only: nil, guard: nil, unless: nil, except: nil, skip: nil, **bindings)
|
|
359
|
+
# Parse args: all symbols/strings are keys, last callable is handler
|
|
360
|
+
key_names = [] #: Array[Symbol | String]
|
|
269
361
|
handler = nil
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
362
|
+
action_name = action
|
|
363
|
+
|
|
364
|
+
args.each do |arg|
|
|
365
|
+
if arg.is_a?(Hash)
|
|
366
|
+
# Hash passed positionally: key({q: -> { ... }})
|
|
367
|
+
bindings.merge!(arg)
|
|
368
|
+
elsif arg.respond_to?(:call)
|
|
369
|
+
handler = arg
|
|
370
|
+
elsif arg.is_a?(Symbol) || arg.is_a?(String)
|
|
371
|
+
# Could be a key name or action name (positional action from old API)
|
|
372
|
+
key_names << arg
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Keyword syntax: key ctrl_c: -> { ... }
|
|
377
|
+
# Each kwarg is key_name => handler
|
|
378
|
+
bindings.each do |key_name, handler_or_action|
|
|
379
|
+
if handler_or_action.respond_to?(:call)
|
|
380
|
+
register_key_handler(key_name, handler_or_action, nil, route, nil)
|
|
381
|
+
else
|
|
382
|
+
register_key_handler(key_name, nil, handler_or_action, route, nil)
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# If we had keyword bindings, skip positional processing
|
|
387
|
+
return if bindings.any?
|
|
388
|
+
|
|
389
|
+
# Old API: key :q, :quit - last symbol is the action
|
|
390
|
+
if handler.nil? && action_name.nil? && key_names.size >= 2
|
|
391
|
+
# Check if last "key" is actually an action by seeing if it looks like a handler
|
|
392
|
+
action_name = key_names.pop
|
|
275
393
|
end
|
|
276
394
|
|
|
277
395
|
guards = @guard_stack.dup
|
|
@@ -293,14 +411,24 @@ module Rooibos
|
|
|
293
411
|
-> (model) { guards.all? { |g| g.call(model) } }
|
|
294
412
|
end
|
|
295
413
|
|
|
414
|
+
# Register each key
|
|
415
|
+
key_names.each do |key_name|
|
|
416
|
+
register_key_handler(key_name, handler, action_name, route, combined_guard)
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
private def register_key_handler(key_name, handler, action_name, route, guard)
|
|
296
421
|
@handlers[key_name.to_s] = KeyHandlerConfig.new(
|
|
297
422
|
handler:,
|
|
298
|
-
action
|
|
423
|
+
action: action_name,
|
|
299
424
|
route:,
|
|
300
|
-
guard:
|
|
425
|
+
guard:
|
|
301
426
|
)
|
|
302
427
|
end
|
|
303
428
|
|
|
429
|
+
# Alias for key (reads better with multiple keys)
|
|
430
|
+
alias_method :keys, :key
|
|
431
|
+
|
|
304
432
|
# Applies a guard to all keys in the block.
|
|
305
433
|
#
|
|
306
434
|
# [when/if/only/guard] Guard that runs if truthy.
|
data/lib/rooibos/version.rb
CHANGED
data/sig/rooibos/message.rbs
CHANGED
|
@@ -181,5 +181,22 @@ module Rooibos
|
|
|
181
181
|
def open?: () -> bool
|
|
182
182
|
def deconstruct_keys: (Array[Symbol]? keys) -> Hash[Symbol, untyped]
|
|
183
183
|
end
|
|
184
|
+
|
|
185
|
+
# Routed message from parent Router to child fragment.
|
|
186
|
+
class Routed < Data
|
|
187
|
+
include Predicates
|
|
188
|
+
|
|
189
|
+
attr_reader envelope: Symbol
|
|
190
|
+
attr_reader event: RatatuiRuby::Event?
|
|
191
|
+
|
|
192
|
+
def self.new: (envelope: Symbol, event: RatatuiRuby::Event?) -> instance
|
|
193
|
+
|
|
194
|
+
def routed?: () -> bool
|
|
195
|
+
def deconstruct_keys: (Array[Symbol]? keys) -> { type: Symbol, envelope: Symbol, event: RatatuiRuby::Event? }
|
|
196
|
+
|
|
197
|
+
# Predicate methods via method_missing (e.g., move_down?, go_back?)
|
|
198
|
+
def respond_to_missing?: (Symbol name, ?bool include_private) -> bool
|
|
199
|
+
def method_missing: (Symbol name, *untyped args, **untyped kwargs) ?{ () -> untyped } -> bool
|
|
200
|
+
end
|
|
184
201
|
end
|
|
185
202
|
end
|
data/sig/rooibos/router.rbs
CHANGED
|
@@ -48,18 +48,31 @@ module Rooibos
|
|
|
48
48
|
module ClassMethods : Module
|
|
49
49
|
@routes: Hash[Symbol, Module]
|
|
50
50
|
@actions: Hash[Symbol, ^() -> Command::execution?]
|
|
51
|
-
@
|
|
51
|
+
@routed_actions: Hash[Symbol, Module]
|
|
52
|
+
@key_handlers: Hash[Symbol, KeyHandlerConfig]
|
|
52
53
|
@scroll_handlers: Hash[Symbol, ScrollHandlerConfig]
|
|
53
54
|
@click_handler: ClickHandlerConfig?
|
|
54
55
|
|
|
55
|
-
def route: (Symbol | String
|
|
56
|
+
def route: (Symbol | String fragment_model_instance_attr, to: Module) -> void
|
|
56
57
|
def routes: () -> Hash[Symbol, Module]
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
# Action: multiple syntax forms supported
|
|
60
|
+
def action: (
|
|
61
|
+
?(Symbol | String | ^() -> Command::execution?) name,
|
|
62
|
+
?(^() -> Command::execution? | Module) value,
|
|
63
|
+
?keymap: (Symbol | Array[Symbol])?,
|
|
64
|
+
?key: (Symbol | Array[Symbol])?,
|
|
65
|
+
?keys: (Symbol | Array[Symbol])?,
|
|
66
|
+
?mousemap: (Symbol | Array[Symbol])?,
|
|
67
|
+
**(^() -> Command::execution? | Module) kwargs
|
|
68
|
+
) -> void
|
|
59
69
|
def actions: () -> Hash[Symbol, ^() -> Command::execution?]
|
|
70
|
+
def routed_actions: () -> Hash[Symbol, Module]
|
|
71
|
+
|
|
72
|
+
private def register_action: (Symbol name, (^() -> Command::execution? | Module) value) -> void
|
|
60
73
|
|
|
61
74
|
def keymap: () { (KeymapBuilder) [self: KeymapBuilder] -> void } -> void
|
|
62
|
-
def key_handlers: () -> Hash[
|
|
75
|
+
def key_handlers: () -> Hash[Symbol, KeyHandlerConfig]
|
|
63
76
|
|
|
64
77
|
def mousemap: () { (MousemapBuilder) [self: MousemapBuilder] -> void } -> void
|
|
65
78
|
def scroll_handlers: () -> Hash[Symbol, ScrollHandlerConfig]
|
|
@@ -76,14 +89,16 @@ module Rooibos
|
|
|
76
89
|
class RouterUpdate
|
|
77
90
|
@routes: Hash[Symbol, Module]
|
|
78
91
|
@actions: Hash[Symbol, ^() -> Command::execution?]
|
|
79
|
-
@
|
|
92
|
+
@routed_actions: Hash[Symbol, Module]
|
|
93
|
+
@key_handlers: Hash[Symbol, KeyHandlerConfig]
|
|
80
94
|
@scroll_handlers: Hash[Symbol, ScrollHandlerConfig]
|
|
81
95
|
@click_handler: ClickHandlerConfig?
|
|
82
96
|
|
|
83
97
|
def initialize: (
|
|
84
98
|
routes: Hash[Symbol, Module],
|
|
85
99
|
actions: Hash[Symbol, ^() -> Command::execution?],
|
|
86
|
-
|
|
100
|
+
routed_actions: Hash[Symbol, Module],
|
|
101
|
+
key_handlers: Hash[Symbol, KeyHandlerConfig],
|
|
87
102
|
scroll_handlers: Hash[Symbol, ScrollHandlerConfig],
|
|
88
103
|
click_handler: ClickHandlerConfig?
|
|
89
104
|
) -> void
|
|
@@ -104,9 +119,10 @@ module Rooibos
|
|
|
104
119
|
|
|
105
120
|
def initialize: () -> void
|
|
106
121
|
|
|
122
|
+
# Variadic key binding: accepts multiple keys, optional action kwarg, hash bindings
|
|
107
123
|
def key: (
|
|
108
|
-
String | Symbol
|
|
109
|
-
|
|
124
|
+
*(String | Symbol | ^() -> Command::execution? | Hash[Symbol, (^() -> Command::execution?) | Symbol]) args,
|
|
125
|
+
?action: Symbol?,
|
|
110
126
|
?route: Symbol?,
|
|
111
127
|
?when: (^(_DataModel) -> bool)?,
|
|
112
128
|
?if: (^(_DataModel) -> bool)?,
|
|
@@ -114,9 +130,13 @@ module Rooibos
|
|
|
114
130
|
?guard: (^(_DataModel) -> bool)?,
|
|
115
131
|
?unless: (^(_DataModel) -> bool)?,
|
|
116
132
|
?except: (^(_DataModel) -> bool)?,
|
|
117
|
-
?skip: (^(_DataModel) -> bool)
|
|
133
|
+
?skip: (^(_DataModel) -> bool)?,
|
|
134
|
+
**(^() -> Command::execution? | Symbol) bindings
|
|
118
135
|
) -> void
|
|
119
136
|
|
|
137
|
+
# Alias for key (reads better with multiple keys)
|
|
138
|
+
alias keys key
|
|
139
|
+
|
|
120
140
|
def only: (
|
|
121
141
|
?when: (^(_DataModel) -> bool)?,
|
|
122
142
|
?if: (^(_DataModel) -> bool)?,
|
|
@@ -134,6 +154,7 @@ module Rooibos
|
|
|
134
154
|
private
|
|
135
155
|
|
|
136
156
|
def with_guard: ((^(_DataModel) -> bool)?) { () -> void } -> void
|
|
157
|
+
def register_key_handler: ((Symbol | String) key_name, (^() -> Command::execution? | Symbol)? handler, (Symbol | String | ^() -> Command::execution?)? action_name, Symbol? route, (^(_DataModel) -> bool)? guard) -> void
|
|
137
158
|
end
|
|
138
159
|
|
|
139
160
|
# Builder for mousemap DSL.
|
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.6.
|
|
4
|
+
version: 0.6.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kerrick Long
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '1.
|
|
18
|
+
version: '1.3'
|
|
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: '1.
|
|
25
|
+
version: '1.3'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: concurrent-ruby
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -278,6 +278,7 @@ files:
|
|
|
278
278
|
- doc/contributors/documentation_stub_audit.md
|
|
279
279
|
- doc/contributors/documentation_style.md
|
|
280
280
|
- doc/contributors/e2e_pty.md
|
|
281
|
+
- doc/contributors/maybe_stateful_router.md
|
|
281
282
|
- doc/contributors/specs/earliest_tutorial_steps_per_story.md
|
|
282
283
|
- doc/contributors/specs/file_browser.md
|
|
283
284
|
- doc/contributors/specs/file_browser_stories.md
|
|
@@ -360,7 +361,6 @@ files:
|
|
|
360
361
|
- doc/tutorial/29_configuration.md
|
|
361
362
|
- doc/tutorial/30_going_further.md
|
|
362
363
|
- doc/tutorial/index.md
|
|
363
|
-
- examples/app_file_browser/app.rb
|
|
364
364
|
- examples/app_fractal_dashboard/README.md
|
|
365
365
|
- examples/app_fractal_dashboard/app.rb
|
|
366
366
|
- examples/app_fractal_dashboard/dashboard/base.rb
|
|
@@ -376,6 +376,10 @@ files:
|
|
|
376
376
|
- examples/app_fractal_dashboard/fragments/stats_panel.rb
|
|
377
377
|
- examples/app_fractal_dashboard/fragments/system_info.rb
|
|
378
378
|
- examples/app_fractal_dashboard/fragments/uptime.rb
|
|
379
|
+
- examples/tutorial/01/app.rb
|
|
380
|
+
- examples/tutorial/02/app.rb
|
|
381
|
+
- examples/tutorial/03/app.rb
|
|
382
|
+
- examples/tutorial/06_safe_refactoring/app.rb
|
|
379
383
|
- examples/verify_readme_usage/README.md
|
|
380
384
|
- examples/verify_readme_usage/app.rb
|
|
381
385
|
- examples/verify_website_first_app/app.rb
|
|
@@ -405,6 +409,7 @@ files:
|
|
|
405
409
|
- lib/rooibos/message/error.rb
|
|
406
410
|
- lib/rooibos/message/http_response.rb
|
|
407
411
|
- lib/rooibos/message/open.rb
|
|
412
|
+
- lib/rooibos/message/routed.rb
|
|
408
413
|
- lib/rooibos/message/system/batch.rb
|
|
409
414
|
- lib/rooibos/message/system/stream.rb
|
|
410
415
|
- lib/rooibos/message/timer.rb
|