anima-core 0.3.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.reek.yml +27 -1
- data/CHANGELOG.md +4 -0
- data/README.md +219 -25
- data/agents/codebase-analyzer.md +88 -0
- data/agents/codebase-pattern-finder.md +83 -0
- data/agents/documentation-researcher.md +59 -0
- data/agents/thoughts-analyzer.md +102 -0
- data/agents/web-search-researcher.md +71 -0
- data/anima-core.gemspec +3 -0
- data/app/channels/session_channel.rb +76 -28
- data/app/jobs/agent_request_job.rb +24 -0
- data/app/jobs/analytical_brain_job.rb +33 -0
- data/app/jobs/count_event_tokens_job.rb +1 -1
- data/app/models/concerns/event/broadcasting.rb +20 -2
- data/app/models/event.rb +1 -1
- data/app/models/goal.rb +91 -0
- data/app/models/session.rb +347 -22
- data/config/application.rb +2 -0
- data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
- data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
- data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
- data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
- data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
- data/db/migrate/20260315140843_create_goals.rb +16 -0
- data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
- data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
- data/lib/agent_loop.rb +65 -9
- data/lib/agents/definition.rb +116 -0
- data/lib/agents/registry.rb +106 -0
- data/lib/analytical_brain/runner.rb +276 -0
- data/lib/analytical_brain/tools/activate_skill.rb +52 -0
- data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
- data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
- data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
- data/lib/analytical_brain/tools/finish_goal.rb +62 -0
- data/lib/analytical_brain/tools/read_workflow.rb +58 -0
- data/lib/analytical_brain/tools/rename_session.rb +63 -0
- data/lib/analytical_brain/tools/set_goal.rb +60 -0
- data/lib/analytical_brain/tools/update_goal.rb +60 -0
- data/lib/analytical_brain.rb +23 -0
- data/lib/anima/cli/mcp/secrets.rb +76 -0
- data/lib/anima/cli/mcp.rb +197 -0
- data/lib/anima/cli.rb +4 -0
- data/lib/anima/installer.rb +168 -0
- data/lib/anima/settings.rb +226 -0
- data/lib/anima/version.rb +1 -1
- data/lib/anima.rb +9 -0
- data/lib/credential_store.rb +103 -0
- data/lib/environment_probe.rb +232 -0
- data/lib/llm/client.rb +29 -10
- data/lib/mcp/client_manager.rb +86 -0
- data/lib/mcp/config.rb +213 -0
- data/lib/mcp/health_check.rb +77 -0
- data/lib/mcp/secrets.rb +73 -0
- data/lib/mcp/stdio_transport.rb +206 -0
- data/lib/providers/anthropic.rb +8 -7
- data/lib/shell_session.rb +11 -10
- data/lib/skills/definition.rb +97 -0
- data/lib/skills/registry.rb +105 -0
- data/lib/tools/edit.rb +3 -4
- data/lib/tools/mcp_tool.rb +114 -0
- data/lib/tools/read.rb +15 -16
- data/lib/tools/registry.rb +14 -12
- data/lib/tools/request_feature.rb +121 -0
- data/lib/tools/return_result.rb +81 -0
- data/lib/tools/spawn_specialist.rb +109 -0
- data/lib/tools/spawn_subagent.rb +111 -0
- data/lib/tools/subagent_prompts.rb +12 -0
- data/lib/tools/web_get.rb +8 -9
- data/lib/tui/app.rb +332 -43
- data/lib/tui/message_store.rb +20 -0
- data/lib/tui/screens/chat.rb +207 -20
- data/lib/workflows/definition.rb +97 -0
- data/lib/workflows/registry.rb +89 -0
- data/skills/activerecord/SKILL.md +255 -0
- data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
- data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
- data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
- data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
- data/skills/activerecord/examples/associations/self_referential.rb +302 -0
- data/skills/activerecord/examples/associations/through_associations.rb +203 -0
- data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
- data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
- data/skills/activerecord/examples/basics/inheritance.rb +377 -0
- data/skills/activerecord/examples/basics/type_casting.rb +317 -0
- data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
- data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
- data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
- data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
- data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
- data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
- data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
- data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
- data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
- data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
- data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
- data/skills/activerecord/examples/querying/optimization.rb +275 -0
- data/skills/activerecord/examples/querying/scopes.rb +260 -0
- data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
- data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
- data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
- data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
- data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
- data/skills/activerecord/references/associations.md +709 -0
- data/skills/activerecord/references/basics.md +622 -0
- data/skills/activerecord/references/callbacks.md +738 -0
- data/skills/activerecord/references/migrations.md +657 -0
- data/skills/activerecord/references/querying.md +655 -0
- data/skills/activerecord/references/validations.md +596 -0
- data/skills/dragonruby/SKILL.md +250 -0
- data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
- data/skills/dragonruby/examples/audio/background_music.rb +29 -0
- data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
- data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
- data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
- data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
- data/skills/dragonruby/examples/core/hello_world.rb +24 -0
- data/skills/dragonruby/examples/core/labels.rb +22 -0
- data/skills/dragonruby/examples/core/sprites.rb +35 -0
- data/skills/dragonruby/examples/core/state_management.rb +29 -0
- data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
- data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
- data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
- data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
- data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
- data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
- data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
- data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
- data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
- data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
- data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
- data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
- data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
- data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
- data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
- data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
- data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
- data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
- data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
- data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
- data/skills/dragonruby/examples/input/controller_input.rb +28 -0
- data/skills/dragonruby/examples/input/directional_input.rb +24 -0
- data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
- data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
- data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
- data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
- data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
- data/skills/dragonruby/examples/rendering/labels.rb +32 -0
- data/skills/dragonruby/examples/rendering/layering.rb +51 -0
- data/skills/dragonruby/examples/rendering/solids.rb +61 -0
- data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
- data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
- data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
- data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
- data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
- data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
- data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
- data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
- data/skills/dragonruby/references/audio.md +396 -0
- data/skills/dragonruby/references/core.md +385 -0
- data/skills/dragonruby/references/distribution.md +434 -0
- data/skills/dragonruby/references/entities.md +516 -0
- data/skills/dragonruby/references/game-logic/persistence.md +386 -0
- data/skills/dragonruby/references/game-logic/state.md +389 -0
- data/skills/dragonruby/references/input.md +414 -0
- data/skills/dragonruby/references/rendering/animation.md +467 -0
- data/skills/dragonruby/references/rendering/primitives.md +403 -0
- data/skills/dragonruby/references/scenes.md +443 -0
- data/skills/draper-decorators/SKILL.md +344 -0
- data/skills/draper-decorators/examples/application_decorator.rb +61 -0
- data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
- data/skills/draper-decorators/examples/model_decorator.rb +152 -0
- data/skills/draper-decorators/references/anti-patterns.md +640 -0
- data/skills/draper-decorators/references/patterns.md +507 -0
- data/skills/draper-decorators/references/testing.md +559 -0
- data/skills/gh-issue.md +182 -0
- data/skills/mcp-server/SKILL.md +177 -0
- data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
- data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
- data/skills/mcp-server/examples/http_client.rb +48 -0
- data/skills/mcp-server/examples/http_server.rb +97 -0
- data/skills/mcp-server/examples/rails_integration.rb +88 -0
- data/skills/mcp-server/examples/stdio_server.rb +108 -0
- data/skills/mcp-server/examples/streaming_client.rb +95 -0
- data/skills/mcp-server/references/gotchas.md +183 -0
- data/skills/mcp-server/references/prompts.md +98 -0
- data/skills/mcp-server/references/resources.md +53 -0
- data/skills/mcp-server/references/server.md +140 -0
- data/skills/mcp-server/references/tools.md +146 -0
- data/skills/mcp-server/references/transport.md +104 -0
- data/skills/ratatui-ruby/SKILL.md +315 -0
- data/skills/ratatui-ruby/references/core-concepts.md +340 -0
- data/skills/ratatui-ruby/references/events.md +387 -0
- data/skills/ratatui-ruby/references/frameworks.md +522 -0
- data/skills/ratatui-ruby/references/layout.md +423 -0
- data/skills/ratatui-ruby/references/styling.md +268 -0
- data/skills/ratatui-ruby/references/testing.md +433 -0
- data/skills/ratatui-ruby/references/widgets.md +532 -0
- data/skills/rspec/SKILL.md +340 -0
- data/skills/rspec/examples/core/basic_structure.rb +69 -0
- data/skills/rspec/examples/core/configuration.rb +126 -0
- data/skills/rspec/examples/core/hooks.rb +126 -0
- data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
- data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
- data/skills/rspec/examples/core/shared_examples.rb +145 -0
- data/skills/rspec/examples/factory_bot/associations.rb +314 -0
- data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
- data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
- data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
- data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
- data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
- data/skills/rspec/examples/factory_bot/traits.rb +293 -0
- data/skills/rspec/examples/factory_bot/transients.rb +229 -0
- data/skills/rspec/examples/matchers/change.rb +115 -0
- data/skills/rspec/examples/matchers/collections.rb +154 -0
- data/skills/rspec/examples/matchers/comparisons.rb +79 -0
- data/skills/rspec/examples/matchers/composing.rb +155 -0
- data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
- data/skills/rspec/examples/matchers/equality.rb +58 -0
- data/skills/rspec/examples/matchers/errors.rb +136 -0
- data/skills/rspec/examples/matchers/output.rb +103 -0
- data/skills/rspec/examples/matchers/predicates.rb +87 -0
- data/skills/rspec/examples/matchers/truthiness.rb +101 -0
- data/skills/rspec/examples/matchers/types.rb +82 -0
- data/skills/rspec/examples/matchers/yield.rb +147 -0
- data/skills/rspec/examples/mocks/any_instance.rb +172 -0
- data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
- data/skills/rspec/examples/mocks/constants.rb +177 -0
- data/skills/rspec/examples/mocks/doubles.rb +139 -0
- data/skills/rspec/examples/mocks/expectations.rb +137 -0
- data/skills/rspec/examples/mocks/message_chains.rb +173 -0
- data/skills/rspec/examples/mocks/ordering.rb +144 -0
- data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
- data/skills/rspec/examples/mocks/responses.rb +223 -0
- data/skills/rspec/examples/mocks/spies.rb +149 -0
- data/skills/rspec/examples/mocks/stubbing.rb +133 -0
- data/skills/rspec/examples/rails/channels.rb +250 -0
- data/skills/rspec/examples/rails/controller_specs.rb +302 -0
- data/skills/rspec/examples/rails/helper_specs.rb +245 -0
- data/skills/rspec/examples/rails/job_specs.rb +256 -0
- data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
- data/skills/rspec/examples/rails/matchers.rb +374 -0
- data/skills/rspec/examples/rails/model_specs.rb +193 -0
- data/skills/rspec/examples/rails/request_specs.rb +275 -0
- data/skills/rspec/examples/rails/routing_specs.rb +276 -0
- data/skills/rspec/examples/rails/system_specs.rb +294 -0
- data/skills/rspec/examples/rails/transactions.rb +254 -0
- data/skills/rspec/examples/rails/view_specs.rb +252 -0
- data/skills/rspec/references/core.md +816 -0
- data/skills/rspec/references/factory_bot.md +641 -0
- data/skills/rspec/references/matchers.md +516 -0
- data/skills/rspec/references/mocks.md +381 -0
- data/skills/rspec/references/rails.md +528 -0
- data/templates/soul.md +40 -0
- data/workflows/commit.md +45 -0
- data/workflows/create_handoff.md +98 -0
- data/workflows/create_note.md +82 -0
- data/workflows/create_plan.md +457 -0
- data/workflows/decompose_ticket.md +109 -0
- data/workflows/feature.md +91 -0
- data/workflows/implement_plan.md +87 -0
- data/workflows/iterate_plan.md +247 -0
- data/workflows/research_codebase.md +210 -0
- data/workflows/resume_handoff.md +217 -0
- data/workflows/review_pr.md +320 -0
- data/workflows/thoughts_init.md +71 -0
- data/workflows/validate_plan.md +166 -0
- metadata +284 -1
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
# Frameworks: Tea and Kit
|
|
2
|
+
|
|
3
|
+
RatatuiRuby ecosystem includes two architectural frameworks: **Tea** (MVU/functional) and **Kit** (component-based/OOP). Both build on the core rendering engine.
|
|
4
|
+
|
|
5
|
+
> **Note:** These frameworks are **separate gems**, not part of the core `ratatui_ruby` gem.
|
|
6
|
+
|
|
7
|
+
## Tea: Model-View-Update
|
|
8
|
+
|
|
9
|
+
Functional architecture emphasizing immutability and pure functions. Also called The Elm Architecture.
|
|
10
|
+
|
|
11
|
+
**Status:** v0.2.0 Pre-Release (ALPHA)
|
|
12
|
+
|
|
13
|
+
**Installation:** `gem install ratatui_ruby-tea` (provides `RatatuiRuby::TEA`)
|
|
14
|
+
|
|
15
|
+
**Repository:** https://git.sr.ht/~kerrick/ratatui_ruby-tea
|
|
16
|
+
|
|
17
|
+
### Core Principle
|
|
18
|
+
|
|
19
|
+
"View as a Function of State" — UI is a pure function of an immutable model. Given the same state, rendering is identical.
|
|
20
|
+
|
|
21
|
+
### Architecture Components
|
|
22
|
+
|
|
23
|
+
#### Model: Immutable State
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
Model = Data.define(:text, :count, :files, :error)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
All state lives in a single frozen object. Updates create new instances.
|
|
30
|
+
|
|
31
|
+
#### Init: Initialization
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
Init = -> do
|
|
35
|
+
Model.new(
|
|
36
|
+
text: "Hello! Press 'q' to quit.",
|
|
37
|
+
count: 0,
|
|
38
|
+
files: [],
|
|
39
|
+
error: nil
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
With initial command:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
Init = -> do
|
|
48
|
+
model = Model.new(text: "Loading...", files: [])
|
|
49
|
+
command = RatatuiRuby::Tea::Command.system('ls -la', :got_files)
|
|
50
|
+
[model, command]
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
#### View: Pure Rendering
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
View = -> (model, tui) do
|
|
58
|
+
tui.paragraph(
|
|
59
|
+
text: model.text,
|
|
60
|
+
alignment: :center,
|
|
61
|
+
block: tui.block(
|
|
62
|
+
title: "My App",
|
|
63
|
+
borders: [:all],
|
|
64
|
+
border_style: {fg: "cyan"}
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Views are pure functions — no side effects or state mutation.
|
|
71
|
+
|
|
72
|
+
#### Update: Message Handler
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
Update = -> (msg, model) do
|
|
76
|
+
case msg
|
|
77
|
+
in [:got_files, {stdout:, status: 0}]
|
|
78
|
+
[model.with(files: stdout.lines), nil]
|
|
79
|
+
|
|
80
|
+
in [:got_files, {stderr:, status:}]
|
|
81
|
+
[model.with(error: "Exit #{status}: #{stderr}"), nil]
|
|
82
|
+
|
|
83
|
+
in {key: 'q'} | {key: 'ctrl_c'}
|
|
84
|
+
RatatuiRuby::Tea::Command.exit
|
|
85
|
+
|
|
86
|
+
in {key: 'r'}
|
|
87
|
+
[model, RatatuiRuby::Tea::Command.system('ls -la', :got_files)]
|
|
88
|
+
|
|
89
|
+
else
|
|
90
|
+
[model, nil]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
- `[new_model, command]` — State change with side effect
|
|
97
|
+
- `model` — No state change
|
|
98
|
+
- `command` — Side effect without state change
|
|
99
|
+
|
|
100
|
+
### Command System
|
|
101
|
+
|
|
102
|
+
Commands execute off the main thread, producing messages when complete.
|
|
103
|
+
|
|
104
|
+
#### Built-In Commands
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
# Shell execution
|
|
108
|
+
RatatuiRuby::Tea::Command.system('git status', :git_result)
|
|
109
|
+
|
|
110
|
+
# One-shot timer
|
|
111
|
+
RatatuiRuby::Tea::Command.wait(5, :timeout)
|
|
112
|
+
|
|
113
|
+
# Recurring timer
|
|
114
|
+
RatatuiRuby::Tea::Command.tick(1.0, :clock_tick)
|
|
115
|
+
|
|
116
|
+
# HTTP request
|
|
117
|
+
RatatuiRuby::Tea::Command.http(:get, 'https://api.example.com', :got_data)
|
|
118
|
+
|
|
119
|
+
# Parallel execution
|
|
120
|
+
RatatuiRuby::Tea::Command.batch([
|
|
121
|
+
RatatuiRuby::Tea::Command.system('git status', :status),
|
|
122
|
+
RatatuiRuby::Tea::Command.http(:get, 'https://api.example.com', :data)
|
|
123
|
+
])
|
|
124
|
+
|
|
125
|
+
# Sequential execution
|
|
126
|
+
RatatuiRuby::Tea::Command.sequence([
|
|
127
|
+
RatatuiRuby::Tea::Command.system('npm install', :install_done),
|
|
128
|
+
RatatuiRuby::Tea::Command.system('npm test', :test_done)
|
|
129
|
+
])
|
|
130
|
+
|
|
131
|
+
# Exit application
|
|
132
|
+
RatatuiRuby::Tea::Command.exit
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Message Handling Pattern
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
def update(msg, model)
|
|
139
|
+
case msg
|
|
140
|
+
in [:got_files, {stdout:, status: 0}]
|
|
141
|
+
files = stdout.lines.map(&:strip)
|
|
142
|
+
[model.with(files:, loading: false), nil]
|
|
143
|
+
|
|
144
|
+
in [:got_files, {stderr:, status:}]
|
|
145
|
+
[model.with(error: "Failed: #{stderr}", loading: false), nil]
|
|
146
|
+
|
|
147
|
+
in [:clock_tick]
|
|
148
|
+
new_model = model.with(time: Time.now.strftime("%H:%M:%S"))
|
|
149
|
+
# Re-dispatch to continue subscription
|
|
150
|
+
[new_model, RatatuiRuby::Tea::Command.tick(1.0, :clock_tick)]
|
|
151
|
+
|
|
152
|
+
in {key: 'r'}
|
|
153
|
+
[model.with(loading: true), RatatuiRuby::Tea::Command.system('ls', :got_files)]
|
|
154
|
+
|
|
155
|
+
else
|
|
156
|
+
[model, nil]
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Running Tea Application
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
RatatuiRuby::TEA.run(
|
|
165
|
+
model: Init.call,
|
|
166
|
+
update: Update,
|
|
167
|
+
view: View
|
|
168
|
+
)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Fractal Architecture (Large Apps)
|
|
172
|
+
|
|
173
|
+
Decompose into **bags** — modules with Model, INITIAL, UPDATE, VIEW:
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
module Counter
|
|
177
|
+
Model = Data.define(:count)
|
|
178
|
+
INITIAL = Model.new(count: 0)
|
|
179
|
+
|
|
180
|
+
UPDATE = -> (msg, model) do
|
|
181
|
+
case msg
|
|
182
|
+
in :increment
|
|
183
|
+
[model.with(count: model.count + 1), nil]
|
|
184
|
+
in :decrement
|
|
185
|
+
[model.with(count: model.count - 1), nil]
|
|
186
|
+
else
|
|
187
|
+
[model, nil]
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
VIEW = -> (model, tui) do
|
|
192
|
+
tui.paragraph(text: "Count: #{model.count}")
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Parent integrates child bag
|
|
197
|
+
ParentUpdate = -> (msg, model) do
|
|
198
|
+
case msg
|
|
199
|
+
in [:counter, counter_msg]
|
|
200
|
+
new_counter, cmd = Counter::UPDATE.call(counter_msg, model.counter)
|
|
201
|
+
mapped_cmd = cmd&.map { |m| [:counter, m] }
|
|
202
|
+
[model.with(counter: new_counter), mapped_cmd]
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Kit: Component-Based Architecture
|
|
210
|
+
|
|
211
|
+
Retained-mode components owning state and handling events. Similar to React class components, Vue Options API, Qt widgets.
|
|
212
|
+
|
|
213
|
+
**Status:** Coming Soon (design phase)
|
|
214
|
+
|
|
215
|
+
### Core Principle
|
|
216
|
+
|
|
217
|
+
"Encapsulated State" — Components own UI state independently, persisting between frames.
|
|
218
|
+
|
|
219
|
+
### Component Structure
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
class MyButton
|
|
223
|
+
include Kit::Component
|
|
224
|
+
|
|
225
|
+
def initialize(label:)
|
|
226
|
+
@label = label
|
|
227
|
+
@click_count = 0
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def mount
|
|
231
|
+
# Called once when entering tree
|
|
232
|
+
@mounted_at = Time.now
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def render(frame, area)
|
|
236
|
+
frame.render_widget(
|
|
237
|
+
tui.paragraph(text: @label, style: current_style),
|
|
238
|
+
area
|
|
239
|
+
)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def handle_event(event)
|
|
243
|
+
return unless event.key? && event.code == "enter"
|
|
244
|
+
@click_count += 1
|
|
245
|
+
:consumed # Stops propagation
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Component Mixins
|
|
251
|
+
|
|
252
|
+
| Mixin | Purpose |
|
|
253
|
+
|-------|---------|
|
|
254
|
+
| `Kit::KeyboardInteractive` | `focusable?`, `focus_boundary?`, `tab_index` |
|
|
255
|
+
| `Kit::MouseInteractive` | `area`, `contains_point?(x, y)` |
|
|
256
|
+
| `Kit::Lifecycle` | Mount/unmount hooks |
|
|
257
|
+
| `Kit::Visual` | `tui` accessor |
|
|
258
|
+
| `Kit::Stateful` | `state`, `is_focused?`, `hovered?`, `pressed?`, `disabled?` |
|
|
259
|
+
| `Kit::Component` | All mixins combined |
|
|
260
|
+
|
|
261
|
+
### State Management
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
class TextInput
|
|
265
|
+
include Kit::Component
|
|
266
|
+
|
|
267
|
+
def initialize
|
|
268
|
+
@buffer = ""
|
|
269
|
+
@cursor_pos = 0
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def handle_event(event)
|
|
273
|
+
case event
|
|
274
|
+
when {key: /^[a-zA-Z0-9]$/}
|
|
275
|
+
@buffer.insert(@cursor_pos, event.key)
|
|
276
|
+
@cursor_pos += 1
|
|
277
|
+
:consumed
|
|
278
|
+
|
|
279
|
+
when {key: "backspace"}
|
|
280
|
+
return if @cursor_pos.zero?
|
|
281
|
+
@buffer.slice!(@cursor_pos - 1)
|
|
282
|
+
@cursor_pos -= 1
|
|
283
|
+
:consumed
|
|
284
|
+
|
|
285
|
+
when {key: "left"}
|
|
286
|
+
@cursor_pos = [@cursor_pos - 1, 0].max
|
|
287
|
+
:consumed
|
|
288
|
+
|
|
289
|
+
else
|
|
290
|
+
nil # Propagate
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def render(frame, area)
|
|
295
|
+
display = @buffer.dup.insert(@cursor_pos, "│")
|
|
296
|
+
frame.render_widget(tui.paragraph(text: display), area)
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Declarative Styling
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
class MyButton
|
|
305
|
+
include Kit::Component
|
|
306
|
+
|
|
307
|
+
styles do
|
|
308
|
+
state :focused, fg: :yellow, bold: true
|
|
309
|
+
state :hovered, fg: :blue
|
|
310
|
+
state :pressed, bg: :white, fg: :black
|
|
311
|
+
state :normal, fg: :white
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def render(frame, area)
|
|
315
|
+
frame.render_widget(
|
|
316
|
+
tui.paragraph(text: @label, style: current_style),
|
|
317
|
+
area
|
|
318
|
+
)
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
`current_style` automatically resolves based on interaction state.
|
|
324
|
+
|
|
325
|
+
### Focus Management
|
|
326
|
+
|
|
327
|
+
```ruby
|
|
328
|
+
# Programmatic focus
|
|
329
|
+
Kit.focus.set(button)
|
|
330
|
+
Kit.focus.next # Tab
|
|
331
|
+
Kit.focus.prev # Shift+Tab
|
|
332
|
+
Kit.focus.blur
|
|
333
|
+
|
|
334
|
+
# Focus boundaries (modals)
|
|
335
|
+
Kit.focus.enter_boundary(modal)
|
|
336
|
+
Kit.focus.exit_boundary
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Component focus control:
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
def focusable? = true
|
|
343
|
+
def focus_boundary? = false
|
|
344
|
+
def tab_index = 0 # Positive: explicit order, 0: tree order, -1: skip
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Event Propagation
|
|
348
|
+
|
|
349
|
+
Return values control propagation:
|
|
350
|
+
- `nil` / `false` — Unhandled, propagate to parent
|
|
351
|
+
- Truthy (`:consumed`, `:submitted`) — Handled, stop propagation
|
|
352
|
+
|
|
353
|
+
```ruby
|
|
354
|
+
def handle_event(event)
|
|
355
|
+
# Delegate to focused child first
|
|
356
|
+
result = @child.handle_event(event) if @child.is_focused?
|
|
357
|
+
return result if result
|
|
358
|
+
|
|
359
|
+
# Handle component-level events
|
|
360
|
+
case event
|
|
361
|
+
when {key: "enter"}
|
|
362
|
+
submit_form
|
|
363
|
+
:submitted
|
|
364
|
+
else
|
|
365
|
+
nil # Propagate
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Integration: Adapter Pattern
|
|
373
|
+
|
|
374
|
+
Reuse Tea views in Kit components:
|
|
375
|
+
|
|
376
|
+
```ruby
|
|
377
|
+
# Pure Tea view
|
|
378
|
+
TeaView = -> (model, tui) do
|
|
379
|
+
tui.paragraph(text: "Count: #{model.count}")
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Kit component adapter
|
|
383
|
+
class DashboardWidget
|
|
384
|
+
include Kit::Component
|
|
385
|
+
|
|
386
|
+
def initialize(record)
|
|
387
|
+
@record = record # Mutable
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def render(frame, area)
|
|
391
|
+
# Convert to immutable model
|
|
392
|
+
tea_model = Data.define(:count).new(count: @record.count)
|
|
393
|
+
widget = TeaView.call(tea_model, tui)
|
|
394
|
+
frame.render_widget(widget, area)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def handle_event(event)
|
|
398
|
+
case event
|
|
399
|
+
when {key: "+"}
|
|
400
|
+
@record.update!(count: @record.count + 1)
|
|
401
|
+
:consumed
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## When to Choose Each
|
|
410
|
+
|
|
411
|
+
### Choose Tea When:
|
|
412
|
+
|
|
413
|
+
- State predictability matters (dashboards, installers)
|
|
414
|
+
- Extensive testing required (pure functions are trivially testable)
|
|
415
|
+
- State reproducibility critical (time-travel debugging)
|
|
416
|
+
- Functional programming preferred
|
|
417
|
+
- Single source of truth simplifies reasoning
|
|
418
|
+
|
|
419
|
+
### Choose Kit When:
|
|
420
|
+
|
|
421
|
+
- Component reusability prioritized (shared libraries)
|
|
422
|
+
- Complex multi-panel interfaces (each panel owns state)
|
|
423
|
+
- Rich interactive components (inputs, forms)
|
|
424
|
+
- Object-oriented programming preferred
|
|
425
|
+
- Team distributes component ownership
|
|
426
|
+
|
|
427
|
+
### Decision Matrix
|
|
428
|
+
|
|
429
|
+
| Criterion | Tea | Kit |
|
|
430
|
+
|-----------|---------|-----|
|
|
431
|
+
| State predictability | Excellent | Good |
|
|
432
|
+
| Testability | Exceptional | Good |
|
|
433
|
+
| Component reuse | Limited | Excellent |
|
|
434
|
+
| Complex interactions | Harder | Natural |
|
|
435
|
+
| Learning curve | Steeper | Shallower |
|
|
436
|
+
| Code volume | Larger | Smaller |
|
|
437
|
+
| Debugging | Time-travel | Standard |
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## Comparison with Raw RatatuiRuby.run
|
|
442
|
+
|
|
443
|
+
```ruby
|
|
444
|
+
# Raw approach
|
|
445
|
+
RatatuiRuby.run do |tui|
|
|
446
|
+
loop do
|
|
447
|
+
tui.draw { |frame| frame.render_widget(widget, frame.area) }
|
|
448
|
+
break if tui.poll_event.ctrl_c?
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
**Raw approach:**
|
|
454
|
+
- Maximum control
|
|
455
|
+
- Minimal abstraction
|
|
456
|
+
- Best for simple scripts and prototypes
|
|
457
|
+
|
|
458
|
+
**Tea:**
|
|
459
|
+
- Declarative MVU
|
|
460
|
+
- Automatic re-rendering
|
|
461
|
+
- Command system for side effects
|
|
462
|
+
|
|
463
|
+
**Kit:**
|
|
464
|
+
- Stateful components
|
|
465
|
+
- Focus/hover tracking
|
|
466
|
+
- Event propagation system
|
|
467
|
+
|
|
468
|
+
Choose raw for prototypes. Use Tea or Kit for production applications.
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## Complete Tea Example
|
|
473
|
+
|
|
474
|
+
> Requires the `ratatui_ruby-tea` gem.
|
|
475
|
+
|
|
476
|
+
```ruby
|
|
477
|
+
require "ratatui_ruby"
|
|
478
|
+
require "ratatui_ruby/tea"
|
|
479
|
+
|
|
480
|
+
Model = Data.define(:items, :selected, :loading)
|
|
481
|
+
|
|
482
|
+
Init = -> do
|
|
483
|
+
model = Model.new(items: [], selected: 0, loading: true)
|
|
484
|
+
[model, RatatuiRuby::Tea::Command.system('ls', :got_files)]
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
View = -> (model, tui) do
|
|
488
|
+
if model.loading
|
|
489
|
+
tui.paragraph(text: "Loading...")
|
|
490
|
+
else
|
|
491
|
+
tui.list(
|
|
492
|
+
items: model.items,
|
|
493
|
+
block: tui.block(title: "Files", borders: [:all]),
|
|
494
|
+
highlight_style: {fg: "yellow", bold: true}
|
|
495
|
+
)
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
Update = -> (msg, model) do
|
|
500
|
+
case msg
|
|
501
|
+
in [:got_files, {stdout:, status: 0}]
|
|
502
|
+
items = stdout.lines.map(&:strip)
|
|
503
|
+
[model.with(items:, loading: false), nil]
|
|
504
|
+
|
|
505
|
+
in {key: 'j'} | {key: 'down'}
|
|
506
|
+
new_idx = [model.selected + 1, model.items.length - 1].min
|
|
507
|
+
[model.with(selected: new_idx), nil]
|
|
508
|
+
|
|
509
|
+
in {key: 'k'} | {key: 'up'}
|
|
510
|
+
new_idx = [model.selected - 1, 0].max
|
|
511
|
+
[model.with(selected: new_idx), nil]
|
|
512
|
+
|
|
513
|
+
in {key: 'q'} | {key: 'ctrl_c'}
|
|
514
|
+
RatatuiRuby::Tea::Command.exit
|
|
515
|
+
|
|
516
|
+
else
|
|
517
|
+
[model, nil]
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
RatatuiRuby::TEA.run(model: Init.call, update: Update, view: View)
|
|
522
|
+
```
|