anima-core 0.2.1 → 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.
Files changed (280) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +27 -1
  3. data/CHANGELOG.md +19 -0
  4. data/README.md +213 -43
  5. data/agents/codebase-analyzer.md +88 -0
  6. data/agents/codebase-pattern-finder.md +83 -0
  7. data/agents/documentation-researcher.md +59 -0
  8. data/agents/thoughts-analyzer.md +102 -0
  9. data/agents/web-search-researcher.md +71 -0
  10. data/anima-core.gemspec +3 -0
  11. data/app/channels/session_channel.rb +195 -45
  12. data/app/decorators/user_message_decorator.rb +16 -5
  13. data/app/jobs/agent_request_job.rb +55 -2
  14. data/app/jobs/analytical_brain_job.rb +33 -0
  15. data/app/jobs/count_event_tokens_job.rb +15 -4
  16. data/app/models/concerns/event/broadcasting.rb +81 -0
  17. data/app/models/event.rb +20 -1
  18. data/app/models/goal.rb +91 -0
  19. data/app/models/session.rb +366 -21
  20. data/config/application.rb +2 -0
  21. data/config/initializers/event_subscribers.rb +0 -1
  22. data/config/routes.rb +0 -6
  23. data/db/migrate/20260313010000_add_status_to_events.rb +8 -0
  24. data/db/migrate/20260313020000_add_processing_to_sessions.rb +7 -0
  25. data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
  26. data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
  27. data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
  28. data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
  29. data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
  30. data/db/migrate/20260315140843_create_goals.rb +16 -0
  31. data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
  32. data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
  33. data/lib/agent_loop.rb +65 -6
  34. data/lib/agents/definition.rb +116 -0
  35. data/lib/agents/registry.rb +106 -0
  36. data/lib/analytical_brain/runner.rb +276 -0
  37. data/lib/analytical_brain/tools/activate_skill.rb +52 -0
  38. data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
  39. data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
  40. data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
  41. data/lib/analytical_brain/tools/finish_goal.rb +62 -0
  42. data/lib/analytical_brain/tools/read_workflow.rb +58 -0
  43. data/lib/analytical_brain/tools/rename_session.rb +63 -0
  44. data/lib/analytical_brain/tools/set_goal.rb +60 -0
  45. data/lib/analytical_brain/tools/update_goal.rb +60 -0
  46. data/lib/analytical_brain.rb +23 -0
  47. data/lib/anima/cli/mcp/secrets.rb +76 -0
  48. data/lib/anima/cli/mcp.rb +197 -0
  49. data/lib/anima/cli.rb +5 -40
  50. data/lib/anima/installer.rb +168 -0
  51. data/lib/anima/settings.rb +226 -0
  52. data/lib/anima/version.rb +1 -1
  53. data/lib/anima.rb +9 -0
  54. data/lib/credential_store.rb +103 -0
  55. data/lib/environment_probe.rb +232 -0
  56. data/lib/events/subscribers/persister.rb +1 -0
  57. data/lib/events/user_message.rb +17 -0
  58. data/lib/llm/client.rb +29 -10
  59. data/lib/mcp/client_manager.rb +86 -0
  60. data/lib/mcp/config.rb +213 -0
  61. data/lib/mcp/health_check.rb +77 -0
  62. data/lib/mcp/secrets.rb +73 -0
  63. data/lib/mcp/stdio_transport.rb +206 -0
  64. data/lib/providers/anthropic.rb +11 -20
  65. data/lib/shell_session.rb +11 -10
  66. data/lib/skills/definition.rb +97 -0
  67. data/lib/skills/registry.rb +105 -0
  68. data/lib/tools/edit.rb +226 -0
  69. data/lib/tools/mcp_tool.rb +114 -0
  70. data/lib/tools/read.rb +151 -0
  71. data/lib/tools/registry.rb +14 -12
  72. data/lib/tools/request_feature.rb +121 -0
  73. data/lib/tools/return_result.rb +81 -0
  74. data/lib/tools/spawn_specialist.rb +109 -0
  75. data/lib/tools/spawn_subagent.rb +111 -0
  76. data/lib/tools/subagent_prompts.rb +12 -0
  77. data/lib/tools/web_get.rb +8 -9
  78. data/lib/tools/write.rb +86 -0
  79. data/lib/tui/app.rb +985 -26
  80. data/lib/tui/cable_client.rb +69 -31
  81. data/lib/tui/message_store.rb +103 -8
  82. data/lib/tui/screens/chat.rb +293 -45
  83. data/lib/workflows/definition.rb +97 -0
  84. data/lib/workflows/registry.rb +89 -0
  85. data/skills/activerecord/SKILL.md +255 -0
  86. data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
  87. data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
  88. data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
  89. data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
  90. data/skills/activerecord/examples/associations/self_referential.rb +302 -0
  91. data/skills/activerecord/examples/associations/through_associations.rb +203 -0
  92. data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
  93. data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
  94. data/skills/activerecord/examples/basics/inheritance.rb +377 -0
  95. data/skills/activerecord/examples/basics/type_casting.rb +317 -0
  96. data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
  97. data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
  98. data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
  99. data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
  100. data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
  101. data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
  102. data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
  103. data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
  104. data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
  105. data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
  106. data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
  107. data/skills/activerecord/examples/querying/optimization.rb +275 -0
  108. data/skills/activerecord/examples/querying/scopes.rb +260 -0
  109. data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
  110. data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
  111. data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
  112. data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
  113. data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
  114. data/skills/activerecord/references/associations.md +709 -0
  115. data/skills/activerecord/references/basics.md +622 -0
  116. data/skills/activerecord/references/callbacks.md +738 -0
  117. data/skills/activerecord/references/migrations.md +657 -0
  118. data/skills/activerecord/references/querying.md +655 -0
  119. data/skills/activerecord/references/validations.md +596 -0
  120. data/skills/dragonruby/SKILL.md +250 -0
  121. data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
  122. data/skills/dragonruby/examples/audio/background_music.rb +29 -0
  123. data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
  124. data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
  125. data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
  126. data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
  127. data/skills/dragonruby/examples/core/hello_world.rb +24 -0
  128. data/skills/dragonruby/examples/core/labels.rb +22 -0
  129. data/skills/dragonruby/examples/core/sprites.rb +35 -0
  130. data/skills/dragonruby/examples/core/state_management.rb +29 -0
  131. data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
  132. data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
  133. data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
  134. data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
  135. data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
  136. data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
  137. data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
  138. data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
  139. data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
  140. data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
  141. data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
  142. data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
  143. data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
  144. data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
  145. data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
  146. data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
  147. data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
  148. data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
  149. data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
  150. data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
  151. data/skills/dragonruby/examples/input/controller_input.rb +28 -0
  152. data/skills/dragonruby/examples/input/directional_input.rb +24 -0
  153. data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
  154. data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
  155. data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
  156. data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
  157. data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
  158. data/skills/dragonruby/examples/rendering/labels.rb +32 -0
  159. data/skills/dragonruby/examples/rendering/layering.rb +51 -0
  160. data/skills/dragonruby/examples/rendering/solids.rb +61 -0
  161. data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
  162. data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
  163. data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
  164. data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
  165. data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
  166. data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
  167. data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
  168. data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
  169. data/skills/dragonruby/references/audio.md +396 -0
  170. data/skills/dragonruby/references/core.md +385 -0
  171. data/skills/dragonruby/references/distribution.md +434 -0
  172. data/skills/dragonruby/references/entities.md +516 -0
  173. data/skills/dragonruby/references/game-logic/persistence.md +386 -0
  174. data/skills/dragonruby/references/game-logic/state.md +389 -0
  175. data/skills/dragonruby/references/input.md +414 -0
  176. data/skills/dragonruby/references/rendering/animation.md +467 -0
  177. data/skills/dragonruby/references/rendering/primitives.md +403 -0
  178. data/skills/dragonruby/references/scenes.md +443 -0
  179. data/skills/draper-decorators/SKILL.md +344 -0
  180. data/skills/draper-decorators/examples/application_decorator.rb +61 -0
  181. data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
  182. data/skills/draper-decorators/examples/model_decorator.rb +152 -0
  183. data/skills/draper-decorators/references/anti-patterns.md +640 -0
  184. data/skills/draper-decorators/references/patterns.md +507 -0
  185. data/skills/draper-decorators/references/testing.md +559 -0
  186. data/skills/gh-issue.md +182 -0
  187. data/skills/mcp-server/SKILL.md +177 -0
  188. data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
  189. data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
  190. data/skills/mcp-server/examples/http_client.rb +48 -0
  191. data/skills/mcp-server/examples/http_server.rb +97 -0
  192. data/skills/mcp-server/examples/rails_integration.rb +88 -0
  193. data/skills/mcp-server/examples/stdio_server.rb +108 -0
  194. data/skills/mcp-server/examples/streaming_client.rb +95 -0
  195. data/skills/mcp-server/references/gotchas.md +183 -0
  196. data/skills/mcp-server/references/prompts.md +98 -0
  197. data/skills/mcp-server/references/resources.md +53 -0
  198. data/skills/mcp-server/references/server.md +140 -0
  199. data/skills/mcp-server/references/tools.md +146 -0
  200. data/skills/mcp-server/references/transport.md +104 -0
  201. data/skills/ratatui-ruby/SKILL.md +315 -0
  202. data/skills/ratatui-ruby/references/core-concepts.md +340 -0
  203. data/skills/ratatui-ruby/references/events.md +387 -0
  204. data/skills/ratatui-ruby/references/frameworks.md +522 -0
  205. data/skills/ratatui-ruby/references/layout.md +423 -0
  206. data/skills/ratatui-ruby/references/styling.md +268 -0
  207. data/skills/ratatui-ruby/references/testing.md +433 -0
  208. data/skills/ratatui-ruby/references/widgets.md +532 -0
  209. data/skills/rspec/SKILL.md +340 -0
  210. data/skills/rspec/examples/core/basic_structure.rb +69 -0
  211. data/skills/rspec/examples/core/configuration.rb +126 -0
  212. data/skills/rspec/examples/core/hooks.rb +126 -0
  213. data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
  214. data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
  215. data/skills/rspec/examples/core/shared_examples.rb +145 -0
  216. data/skills/rspec/examples/factory_bot/associations.rb +314 -0
  217. data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
  218. data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
  219. data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
  220. data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
  221. data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
  222. data/skills/rspec/examples/factory_bot/traits.rb +293 -0
  223. data/skills/rspec/examples/factory_bot/transients.rb +229 -0
  224. data/skills/rspec/examples/matchers/change.rb +115 -0
  225. data/skills/rspec/examples/matchers/collections.rb +154 -0
  226. data/skills/rspec/examples/matchers/comparisons.rb +79 -0
  227. data/skills/rspec/examples/matchers/composing.rb +155 -0
  228. data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
  229. data/skills/rspec/examples/matchers/equality.rb +58 -0
  230. data/skills/rspec/examples/matchers/errors.rb +136 -0
  231. data/skills/rspec/examples/matchers/output.rb +103 -0
  232. data/skills/rspec/examples/matchers/predicates.rb +87 -0
  233. data/skills/rspec/examples/matchers/truthiness.rb +101 -0
  234. data/skills/rspec/examples/matchers/types.rb +82 -0
  235. data/skills/rspec/examples/matchers/yield.rb +147 -0
  236. data/skills/rspec/examples/mocks/any_instance.rb +172 -0
  237. data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
  238. data/skills/rspec/examples/mocks/constants.rb +177 -0
  239. data/skills/rspec/examples/mocks/doubles.rb +139 -0
  240. data/skills/rspec/examples/mocks/expectations.rb +137 -0
  241. data/skills/rspec/examples/mocks/message_chains.rb +173 -0
  242. data/skills/rspec/examples/mocks/ordering.rb +144 -0
  243. data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
  244. data/skills/rspec/examples/mocks/responses.rb +223 -0
  245. data/skills/rspec/examples/mocks/spies.rb +149 -0
  246. data/skills/rspec/examples/mocks/stubbing.rb +133 -0
  247. data/skills/rspec/examples/rails/channels.rb +250 -0
  248. data/skills/rspec/examples/rails/controller_specs.rb +302 -0
  249. data/skills/rspec/examples/rails/helper_specs.rb +245 -0
  250. data/skills/rspec/examples/rails/job_specs.rb +256 -0
  251. data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
  252. data/skills/rspec/examples/rails/matchers.rb +374 -0
  253. data/skills/rspec/examples/rails/model_specs.rb +193 -0
  254. data/skills/rspec/examples/rails/request_specs.rb +275 -0
  255. data/skills/rspec/examples/rails/routing_specs.rb +276 -0
  256. data/skills/rspec/examples/rails/system_specs.rb +294 -0
  257. data/skills/rspec/examples/rails/transactions.rb +254 -0
  258. data/skills/rspec/examples/rails/view_specs.rb +252 -0
  259. data/skills/rspec/references/core.md +816 -0
  260. data/skills/rspec/references/factory_bot.md +641 -0
  261. data/skills/rspec/references/matchers.md +516 -0
  262. data/skills/rspec/references/mocks.md +381 -0
  263. data/skills/rspec/references/rails.md +528 -0
  264. data/templates/soul.md +40 -0
  265. data/workflows/commit.md +45 -0
  266. data/workflows/create_handoff.md +98 -0
  267. data/workflows/create_note.md +82 -0
  268. data/workflows/create_plan.md +457 -0
  269. data/workflows/decompose_ticket.md +109 -0
  270. data/workflows/feature.md +91 -0
  271. data/workflows/implement_plan.md +87 -0
  272. data/workflows/iterate_plan.md +247 -0
  273. data/workflows/research_codebase.md +210 -0
  274. data/workflows/resume_handoff.md +217 -0
  275. data/workflows/review_pr.md +320 -0
  276. data/workflows/thoughts_init.md +71 -0
  277. data/workflows/validate_plan.md +166 -0
  278. metadata +290 -3
  279. data/app/controllers/api/sessions_controller.rb +0 -25
  280. data/lib/events/subscribers/action_cable_bridge.rb +0 -59
@@ -0,0 +1,340 @@
1
+ # Core Concepts
2
+
3
+ RatatuiRuby provides immediate-mode terminal rendering with managed lifecycle. This reference covers application initialization, terminal lifecycle, viewport modes, and core objects.
4
+
5
+ ## RatatuiRuby.run
6
+
7
+ Entry point handling complete terminal lifecycle with exception safety.
8
+
9
+ ```ruby
10
+ RatatuiRuby.run do |tui|
11
+ loop do
12
+ tui.draw do |frame|
13
+ frame.render_widget(tui.paragraph(text: "Hello"), frame.area)
14
+ end
15
+
16
+ case tui.poll_event
17
+ in {type: :key, code: "q"}
18
+ break
19
+ end
20
+ end
21
+ end
22
+ # Terminal restored automatically
23
+ ```
24
+
25
+ ### Options
26
+
27
+ | Option | Default | Purpose |
28
+ |--------|---------|---------|
29
+ | `focus_events` | `true` | Enable focus change events |
30
+ | `bracketed_paste` | `true` | Enable bracketed paste mode |
31
+ | `viewport` | `nil` | Viewport mode (`nil`/`:fullscreen` or `:inline`) |
32
+ | `height` | `nil` | Lines for inline viewport (required when `viewport: :inline`) |
33
+
34
+ ```ruby
35
+ RatatuiRuby.run(viewport: :inline, height: 8) do |tui|
36
+ # 8-line inline viewport
37
+ end
38
+ ```
39
+
40
+ ### Manual Lifecycle (Advanced)
41
+
42
+ For fine-grained control, use `init_terminal` and `restore_terminal` with `ensure`:
43
+
44
+ ```ruby
45
+ RatatuiRuby.init_terminal
46
+ begin
47
+ RatatuiRuby.draw do |frame|
48
+ frame.render_widget(widget, frame.area)
49
+ end
50
+ ensure
51
+ RatatuiRuby.restore_terminal # Always executes
52
+ end
53
+ ```
54
+
55
+ ## Terminal Lifecycle
56
+
57
+ Three-phase lifecycle prevents terminal corruption.
58
+
59
+ ### Phase 1: Setup (`init_terminal`)
60
+
61
+ - Enters alternate screen (fullscreen) or creates fixed region (inline)
62
+ - Enables raw mode for direct key capture
63
+ - Configures focus events and bracketed paste
64
+ - Sets `@tui_session_active = true`
65
+
66
+ ### Phase 2: Loop
67
+
68
+ Application code executes with access to TUI instance:
69
+ - `draw` renders UI
70
+ - `poll_event` captures input
71
+ - Application logic manages state
72
+
73
+ ### Phase 3: Teardown (`restore_terminal`)
74
+
75
+ Guaranteed cleanup via `ensure` block:
76
+ - Leaves alternate screen
77
+ - Disables raw mode
78
+ - Flushes warnings
79
+ - Sets `@tui_session_active = false`
80
+
81
+ ### Signal Handling
82
+
83
+ - Most signals (SIGTERM, SIGINT) trigger stack unwinding, allowing `ensure` execution
84
+ - **SIGKILL cannot be caught** - may leave terminal in raw mode
85
+ - **Ctrl+C in raw mode** captured as key event, not SIGINT - handle explicitly:
86
+
87
+ ```ruby
88
+ case tui.poll_event
89
+ in {type: :key, code: "c", modifiers: ["ctrl"]}
90
+ break
91
+ end
92
+ ```
93
+
94
+ ## Viewport Modes
95
+
96
+ ### Fullscreen (Default)
97
+
98
+ Takes over entire terminal, clears on exit.
99
+
100
+ ```ruby
101
+ RatatuiRuby.run do |tui|
102
+ # Fullscreen is default
103
+ end
104
+
105
+ # Or explicitly:
106
+ RatatuiRuby.run(viewport: :fullscreen) do |tui|
107
+ # ...
108
+ end
109
+ ```
110
+
111
+ **Characteristics:**
112
+ - Alternate screen buffer for isolated rendering
113
+ - All content cleared on exit
114
+ - No scrollback preservation
115
+ - Full terminal dimensions via `frame.area`
116
+ - Best for complete TUI applications
117
+
118
+ ### Inline Mode
119
+
120
+ Fixed-height region preserving scrollback after exit.
121
+
122
+ ```ruby
123
+ RatatuiRuby.run(viewport: :inline, height: 8) do |tui|
124
+ # 8-line region
125
+ end
126
+ ```
127
+
128
+ **Characteristics:**
129
+ - Fixed height (required parameter)
130
+ - Content persists in scrollback
131
+ - Supports `insert_before` for logging above viewport
132
+ - Best for status displays, progress indicators, inline widgets
133
+
134
+ **Logging during inline mode:**
135
+
136
+ ```ruby
137
+ # Option 1: Pass widget directly
138
+ RatatuiRuby.insert_before(height, widget)
139
+
140
+ # Option 2: Use block to generate widget (block returns widget, doesn't receive frame)
141
+ RatatuiRuby.insert_before(height) do
142
+ tui.paragraph(text: "Log message") # Block returns a widget
143
+ end
144
+ ```
145
+
146
+ ## Frame Object
147
+
148
+ Controlled access to terminal buffer during rendering. Valid only within `draw` block.
149
+
150
+ ### Methods
151
+
152
+ #### `area()`
153
+
154
+ Returns `Rect` of entire drawable region:
155
+
156
+ ```ruby
157
+ tui.draw do |frame|
158
+ puts "Size: #{frame.area.width}x#{frame.area.height}"
159
+ end
160
+ ```
161
+
162
+ #### `render_widget(widget, area)`
163
+
164
+ Renders stateless widget at specified area:
165
+
166
+ ```ruby
167
+ tui.draw do |frame|
168
+ para = tui.paragraph(text: "Content")
169
+ frame.render_widget(para, frame.area)
170
+ end
171
+ ```
172
+
173
+ #### `render_stateful_widget(widget, area, state)`
174
+
175
+ Renders widget with persistent state (scroll position, selection):
176
+
177
+ ```ruby
178
+ @list_state = tui.list_state
179
+
180
+ tui.draw do |frame|
181
+ list = tui.list(items: ["A", "B", "C"])
182
+ frame.render_stateful_widget(list, frame.area, @list_state)
183
+ end
184
+
185
+ # State reflects current selection/offset
186
+ puts "Selected: #{@list_state.selected}"
187
+ ```
188
+
189
+ State object is single source of truth, overriding widget configuration.
190
+
191
+ #### `set_cursor_position(x, y)`
192
+
193
+ Positions cursor at zero-indexed coordinates:
194
+
195
+ ```ruby
196
+ tui.draw do |frame|
197
+ frame.render_widget(tui.paragraph(text: "Name: [alice]"), frame.area)
198
+ frame.set_cursor_position(7, 0) # After "Name: ["
199
+ end
200
+ ```
201
+
202
+ ### Thread Safety
203
+
204
+ Frame is not Ractor-shareable. Valid only during draw block execution.
205
+
206
+ ### Layout Reuse Pattern
207
+
208
+ Store layout results for hit testing:
209
+
210
+ ```ruby
211
+ tui.draw do |frame|
212
+ sidebar, main = tui.layout_split(frame.area, ...)
213
+ frame.render_widget(sidebar_widget, sidebar)
214
+ frame.render_widget(main_widget, main)
215
+ @regions = {sidebar:, main:} # Reuse for click detection
216
+ end
217
+ ```
218
+
219
+ ## TUI Factory Object
220
+
221
+ Manages lifecycle and provides concise factory methods.
222
+
223
+ ### Access
224
+
225
+ ```ruby
226
+ RatatuiRuby.run do |tui|
227
+ # tui is RatatuiRuby::TUI instance
228
+ end
229
+ ```
230
+
231
+ ### Factory Comparison
232
+
233
+ ```ruby
234
+ # Without factories (verbose):
235
+ para = RatatuiRuby::Widgets::Paragraph.new(
236
+ text: "Hello",
237
+ block: RatatuiRuby::Widgets::Block.new(borders: [:all])
238
+ )
239
+
240
+ # With factories (concise):
241
+ para = tui.paragraph(text: "Hello", block: tui.block(borders: [:all]))
242
+ ```
243
+
244
+ ### Factory Modules
245
+
246
+ | Module | Methods | Purpose |
247
+ |--------|---------|---------|
248
+ | **WidgetFactories** | `paragraph`, `block`, `list`, `table`, `gauge` | UI components |
249
+ | **LayoutFactories** | `rect`, `constraint_*`, `layout`, `layout_split` | Layouts |
250
+ | **StyleFactories** | `style` | Formatting |
251
+ | **TextFactories** | `text_span`, `text_line`, `text_width` | Styled text |
252
+ | **StateFactories** | `list_state`, `table_state`, `scrollbar_state` | Widget state |
253
+ | **CanvasFactories** | `shape_map`, `shape_line`, `shape_point` | Canvas shapes |
254
+ | **BufferFactories** | `cell` | Testing |
255
+ | **Core** | `draw`, `poll_event`, `get_cell_at` | Terminal ops |
256
+
257
+ ### DWIM Coercion
258
+
259
+ Factories implement "Do What I Mean" argument coercion with automatic type normalization and validation.
260
+
261
+ ## Module Functions
262
+
263
+ ### Drawing
264
+
265
+ ```ruby
266
+ # Declarative (tree-based)
267
+ RatatuiRuby.draw(widget)
268
+
269
+ # Imperative (block-based)
270
+ RatatuiRuby.draw do |frame|
271
+ frame.render_widget(widget, frame.area)
272
+ end
273
+ ```
274
+
275
+ ### Event Polling
276
+
277
+ ```ruby
278
+ event = RatatuiRuby.poll_event # ~60 FPS default (16ms)
279
+ event = RatatuiRuby.poll_event(timeout: nil) # Block until event
280
+ event = RatatuiRuby.poll_event(timeout: 0.0) # Non-blocking
281
+ ```
282
+
283
+ ### Terminal Inspection
284
+
285
+ | Method | Returns |
286
+ |--------|---------|
287
+ | `get_terminal_size()` | Full terminal dimensions as `Rect` |
288
+ | `get_terminal_area()` | Alias for `get_terminal_size` |
289
+ | `get_viewport_area()` | Current viewport area |
290
+ | `get_viewport_size()` | Alias for `get_viewport_area` |
291
+ | `get_cell_at(x, y)` | `Buffer::Cell` at coordinates (for testing) |
292
+
293
+ ## Complete Example
294
+
295
+ ```ruby
296
+ RatatuiRuby.run do |tui|
297
+ @list_state = tui.list_state(0)
298
+ items = ["Dashboard", "Settings", "Help", "Quit"]
299
+
300
+ loop do
301
+ tui.draw do |frame|
302
+ sidebar, content = tui.layout_split(
303
+ frame.area,
304
+ direction: :horizontal,
305
+ constraints: [tui.constraint_length(20), tui.constraint_min(0)]
306
+ )
307
+
308
+ # Sidebar with stateful list
309
+ list = tui.list(
310
+ items:,
311
+ block: tui.block(title: "Menu", borders: [:all])
312
+ )
313
+ frame.render_stateful_widget(list, sidebar, @list_state)
314
+
315
+ # Content area
316
+ selected_item = items[@list_state.selected] || "None"
317
+ frame.render_widget(
318
+ tui.paragraph(
319
+ text: "Selected: #{selected_item}",
320
+ block: tui.block(title: "Content", borders: [:all])
321
+ ),
322
+ content
323
+ )
324
+ end
325
+
326
+ case tui.poll_event
327
+ in {type: :key, code: "q"}
328
+ break
329
+ in {type: :key, code: "j"} | {type: :key, code: "down"}
330
+ @list_state.select_next
331
+ in {type: :key, code: "k"} | {type: :key, code: "up"}
332
+ @list_state.select_previous
333
+ in {type: :key, code: "c", modifiers: ["ctrl"]}
334
+ break
335
+ else
336
+ # Continue
337
+ end
338
+ end
339
+ end
340
+ ```
@@ -0,0 +1,387 @@
1
+ # Event Handling
2
+
3
+ RatatuiRuby provides a robust event system for keyboard, mouse, resize, paste, and focus events. This reference covers polling, event types, and handling patterns.
4
+
5
+ ## poll_event
6
+
7
+ Primary method for retrieving user input:
8
+
9
+ ```ruby
10
+ event = RatatuiRuby.poll_event(timeout: 0.016)
11
+ ```
12
+
13
+ ### Timeout Options
14
+
15
+ | Value | Behavior |
16
+ |-------|----------|
17
+ | `0.016` (default) | ~60 FPS, smooth rendering loops |
18
+ | `nil` | Block until event arrives |
19
+ | `0.0` | Non-blocking, return immediately |
20
+ | Positive float | Wait specified duration |
21
+
22
+ ### Return Types
23
+
24
+ - `Event::Key` — Keyboard input
25
+ - `Event::Mouse` — Mouse interactions
26
+ - `Event::Resize` — Terminal dimension changes
27
+ - `Event::Paste` — Clipboard content
28
+ - `Event::FocusGained` — Terminal received focus
29
+ - `Event::FocusLost` — Terminal lost focus
30
+ - `Event::None` — No event (Null Object pattern)
31
+
32
+ ## Event Base Class
33
+
34
+ Type-checking predicates:
35
+
36
+ ```ruby
37
+ event.key? # Keyboard event
38
+ event.mouse? # Mouse activity
39
+ event.resize? # Terminal resize
40
+ event.paste? # Clipboard paste
41
+ event.focus_gained? # Focus acquisition
42
+ event.focus_lost? # Focus loss
43
+ event.none? # No event
44
+ ```
45
+
46
+ ## Key Events
47
+
48
+ ### Attributes
49
+
50
+ ```ruby
51
+ event.code # Key identifier: "a", "enter", "up", "f1"
52
+ event.modifiers # Active modifiers: ["ctrl"], ["alt", "shift"]
53
+ event.kind # Category: :standard, :function, :media, :modifier, :system
54
+ ```
55
+
56
+ ### Comparison Methods
57
+
58
+ ```ruby
59
+ # Symbol comparison
60
+ event == :ctrl_c
61
+ event == :shift_up
62
+ event == :q
63
+
64
+ # String comparison
65
+ event == "c"
66
+ event == "enter"
67
+
68
+ # Object comparison
69
+ event == Event::Key.new(code: "c", modifiers: ["ctrl"])
70
+ ```
71
+
72
+ ### Helper Predicates
73
+
74
+ Dynamic methods via `method_missing`:
75
+
76
+ ```ruby
77
+ event.ctrl_c? # Ctrl+C
78
+ event.enter? # Enter key
79
+ event.shift_up? # Shift+Up
80
+ event.q? # "q" key
81
+ event.esc? # Escape
82
+ event.space? # Space bar
83
+ event.tab? # Tab key
84
+ ```
85
+
86
+ DWIM (Do What I Mean) shortcuts:
87
+
88
+ ```ruby
89
+ event.pause? # Matches "pause" and "media_pause"
90
+ event.play? # Matches "media_play"
91
+ ```
92
+
93
+ ### Character Detection
94
+
95
+ ```ruby
96
+ event.text? # True for printable characters
97
+ event.char # Character or nil for special keys
98
+ event.to_s # Character or empty string
99
+ ```
100
+
101
+ ### Key Categories
102
+
103
+ **Navigation:**
104
+ - `up`, `down`, `left`, `right`
105
+ - `home`, `end`, `page_up`, `page_down`
106
+ - `insert`, `delete`
107
+
108
+ **System:**
109
+ - `print_screen`, `pause`, `menu`
110
+
111
+ **Media:**
112
+ - `play`, `media_pause`, `play_pause`, `stop`
113
+ - `track_next`, `track_previous`
114
+ - `mute_volume`, `lower_volume`, `raise_volume`
115
+
116
+ **Modifiers:**
117
+ - `left_shift`, `right_shift`
118
+ - `left_control`, `right_control`
119
+ - `left_alt`, `right_alt`
120
+
121
+ ### Pattern Matching
122
+
123
+ ```ruby
124
+ case event
125
+ in type: :key, code: "c", modifiers: ["ctrl"]
126
+ break
127
+ in type: :key, code: "q"
128
+ break
129
+ in type: :key, kind: :media
130
+ handle_media_key
131
+ in type: :key, code: "j"
132
+ move_down
133
+ in type: :key, code: "k"
134
+ move_up
135
+ end
136
+ ```
137
+
138
+ ## Mouse Events
139
+
140
+ ### Attributes
141
+
142
+ ```ruby
143
+ event.kind # "down", "up", "drag", "moved", "scroll_up", "scroll_down"
144
+ event.button # "left", "right", "middle", "none", nil
145
+ event.x # Column coordinate (zero-indexed)
146
+ event.y # Row coordinate (zero-indexed)
147
+ event.modifiers # Active keyboard modifiers
148
+ ```
149
+
150
+ ### Event Type Methods
151
+
152
+ ```ruby
153
+ event.down? # Button pressed
154
+ event.up? # Button released
155
+ event.drag? # Dragging
156
+ event.scroll_up? # Scroll up
157
+ event.scroll_down? # Scroll down
158
+ ```
159
+
160
+ ### Usage
161
+
162
+ ```ruby
163
+ if event.mouse? && event.down? && event.button == "left"
164
+ handle_click(event.x, event.y)
165
+ end
166
+ ```
167
+
168
+ ### Pattern Matching
169
+
170
+ ```ruby
171
+ case event
172
+ in type: :mouse, kind: "down", button: "left", x:, y:
173
+ handle_click(x, y)
174
+ in type: :mouse, kind: "drag", x:, y:
175
+ handle_drag(x, y)
176
+ in type: :mouse, kind: "scroll_up"
177
+ scroll_up
178
+ in type: :mouse, kind: "scroll_down"
179
+ scroll_down
180
+ end
181
+ ```
182
+
183
+ ## Resize Events
184
+
185
+ ### Attributes
186
+
187
+ ```ruby
188
+ event.width # New terminal width in columns
189
+ event.height # New terminal height in rows
190
+ ```
191
+
192
+ ### Usage
193
+
194
+ ```ruby
195
+ if event.resize?
196
+ recalculate_layout(event.width, event.height)
197
+ end
198
+ ```
199
+
200
+ ### Pattern Matching
201
+
202
+ ```ruby
203
+ case event
204
+ in type: :resize, width:, height:
205
+ update_dimensions(width, height)
206
+ end
207
+ ```
208
+
209
+ ## Paste Events
210
+
211
+ Handles pasted text as atomic action (prevents rapid keystroke flood).
212
+
213
+ ### Attributes
214
+
215
+ ```ruby
216
+ event.content # Pasted text string
217
+ ```
218
+
219
+ ### Usage
220
+
221
+ ```ruby
222
+ if event.paste?
223
+ insert_text(event.content)
224
+ end
225
+ ```
226
+
227
+ ### Pattern Matching
228
+
229
+ ```ruby
230
+ case event
231
+ in type: :paste, content:
232
+ process_pasted_content(content)
233
+ end
234
+ ```
235
+
236
+ ## Focus Events
237
+
238
+ Track terminal window focus state. **Limited terminal support** (iTerm2, Kitty, newer xterm).
239
+
240
+ ### FocusGained
241
+
242
+ ```ruby
243
+ if event.focus_gained?
244
+ resume_animations
245
+ refresh_data
246
+ end
247
+ ```
248
+
249
+ ### FocusLost
250
+
251
+ ```ruby
252
+ if event.focus_lost?
253
+ pause_animations
254
+ reduce_polling_frequency
255
+ end
256
+ ```
257
+
258
+ ### Pattern Matching
259
+
260
+ ```ruby
261
+ case event
262
+ in type: :focus_gained
263
+ restore_foreground_mode
264
+ in type: :focus_lost
265
+ enter_background_mode
266
+ end
267
+ ```
268
+
269
+ ## None Events (Null Object)
270
+
271
+ Returns when no input available. Eliminates nil-checks:
272
+
273
+ ```ruby
274
+ if event.none?
275
+ # Timeout expired, continue rendering
276
+ update_clock_display
277
+ end
278
+
279
+ # Safe to call predicates (all return false)
280
+ event.ctrl_c? # => false
281
+ event.key? # => false
282
+ ```
283
+
284
+ ## Complete Event Loop
285
+
286
+ ```ruby
287
+ RatatuiRuby.run do |tui|
288
+ loop do
289
+ tui.draw { |frame| render_view(model, frame) }
290
+
291
+ case tui.poll_event
292
+ # Exit conditions
293
+ in {type: :key, code: "q"} | {type: :key, code: "c", modifiers: ["ctrl"]}
294
+ break
295
+
296
+ # Navigation
297
+ in type: :key, code: "up" | "k"
298
+ model = move_up(model)
299
+ in type: :key, code: "down" | "j"
300
+ model = move_down(model)
301
+ in type: :key, code: "enter"
302
+ model = activate(model)
303
+
304
+ # Mouse
305
+ in type: :mouse, kind: "down", button: "left", x:, y:
306
+ model = handle_click(model, x, y)
307
+ in type: :mouse, kind: "scroll_up"
308
+ model = scroll_up(model)
309
+ in type: :mouse, kind: "scroll_down"
310
+ model = scroll_down(model)
311
+
312
+ # Terminal
313
+ in type: :resize, width:, height:
314
+ model = update_layout(model, width, height)
315
+ in type: :paste, content:
316
+ model = insert_text(model, content)
317
+
318
+ # Focus
319
+ in type: :focus_lost
320
+ model = enter_background(model)
321
+ in type: :focus_gained
322
+ model = resume_foreground(model)
323
+
324
+ else
325
+ # Catch unmatched events (required to prevent NoMatchingPatternError)
326
+ nil
327
+ end
328
+ end
329
+ end
330
+ ```
331
+
332
+ ## Event Handling Patterns
333
+
334
+ ### Helper Method Approach
335
+
336
+ ```ruby
337
+ event = tui.poll_event
338
+ break if event.ctrl_c?
339
+ @list_state.select_next if event.down? || event.j?
340
+ @list_state.select_previous if event.up? || event.k?
341
+ ```
342
+
343
+ ### Predicate Chain
344
+
345
+ ```ruby
346
+ event = tui.poll_event
347
+ case
348
+ when event.ctrl_c? then break
349
+ when event.enter? then activate_selection
350
+ when event.up?, event.k? then move_up
351
+ when event.down?, event.j? then move_down
352
+ end
353
+ ```
354
+
355
+ ### Mixed Pattern Matching
356
+
357
+ ```ruby
358
+ case tui.poll_event
359
+ in Event::Key if _1.ctrl_c?
360
+ break
361
+ in Event::Key => e if e.text?
362
+ insert_character(e.char)
363
+ in Event::Mouse => e if e.down?
364
+ handle_click(e.x, e.y)
365
+ end
366
+ ```
367
+
368
+ ## Terminal Compatibility Notes
369
+
370
+ Some key combinations intercepted by terminal emulators:
371
+ - Ctrl+PageUp/PageDown (tab switching)
372
+ - Ctrl+Tab
373
+ - Cmd+key (macOS)
374
+
375
+ For broader key support, use Kitty, WezTerm, or Alacritty with enhanced key protocols.
376
+
377
+ ## Quick Reference
378
+
379
+ | Event Type | Key Attributes | Common Predicates |
380
+ |------------|----------------|-------------------|
381
+ | Key | `code`, `modifiers`, `kind` | `ctrl_c?`, `enter?`, `up?`, `down?`, `text?` |
382
+ | Mouse | `kind`, `button`, `x`, `y`, `modifiers` | `down?`, `up?`, `drag?`, `scroll_up?` |
383
+ | Resize | `width`, `height` | `resize?` |
384
+ | Paste | `content` | `paste?` |
385
+ | FocusGained | — | `focus_gained?` |
386
+ | FocusLost | — | `focus_lost?` |
387
+ | None | — | `none?` |