ratatui_ruby 1.3.0 → 1.4.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 (263) hide show
  1. checksums.yaml +4 -4
  2. data/ext/ratatui_ruby/Cargo.lock +2 -1
  3. data/ext/ratatui_ruby/Cargo.toml +2 -1
  4. data/ext/ratatui_ruby/src/events.rs +157 -18
  5. data/lib/ratatui_ruby/test_helper/snapshot.rb +58 -10
  6. data/lib/ratatui_ruby/test_helper/subprocess_timeout.rb +35 -0
  7. data/lib/ratatui_ruby/test_helper.rb +2 -0
  8. data/lib/ratatui_ruby/version.rb +1 -1
  9. metadata +17 -270
  10. data/.builds/ruby-3.2.yml +0 -54
  11. data/.builds/ruby-3.3.yml +0 -54
  12. data/.builds/ruby-3.4.yml +0 -54
  13. data/.builds/ruby-4.0.0.yml +0 -54
  14. data/.pre-commit-config.yaml +0 -16
  15. data/.rubocop.yml +0 -10
  16. data/AGENTS.md +0 -147
  17. data/CHANGELOG.md +0 -771
  18. data/README.md +0 -187
  19. data/README.rdoc +0 -302
  20. data/Rakefile +0 -11
  21. data/Steepfile +0 -50
  22. data/doc/concepts/application_architecture.md +0 -321
  23. data/doc/concepts/application_testing.md +0 -193
  24. data/doc/concepts/async.md +0 -190
  25. data/doc/concepts/custom_widgets.md +0 -247
  26. data/doc/concepts/debugging.md +0 -401
  27. data/doc/concepts/event_handling.md +0 -162
  28. data/doc/concepts/interactive_design.md +0 -146
  29. data/doc/contributors/auditing/parity.md +0 -239
  30. data/doc/contributors/design/ruby_frontend.md +0 -448
  31. data/doc/contributors/design/rust_backend.md +0 -434
  32. data/doc/contributors/design.md +0 -11
  33. data/doc/contributors/developing_examples.md +0 -400
  34. data/doc/contributors/documentation_style.md +0 -121
  35. data/doc/contributors/index.md +0 -21
  36. data/doc/contributors/releasing.md +0 -215
  37. data/doc/contributors/todo/align/api_completeness_audit-finished.md +0 -381
  38. data/doc/contributors/todo/align/api_completeness_audit-unfinished.md +0 -200
  39. data/doc/contributors/todo/align/term.md +0 -351
  40. data/doc/contributors/todo/align/terminal.md +0 -647
  41. data/doc/contributors/todo/future_work.md +0 -169
  42. data/doc/contributors/upstream_requests/paragraph_span_rects.md +0 -259
  43. data/doc/contributors/upstream_requests/tab_rects.md +0 -173
  44. data/doc/contributors/upstream_requests/title_rects.md +0 -132
  45. data/doc/custom.css +0 -22
  46. data/doc/getting_started/quickstart.md +0 -291
  47. data/doc/getting_started/why.md +0 -93
  48. data/doc/images/app_all_events.png +0 -0
  49. data/doc/images/app_cli_rich_moments.gif +0 -0
  50. data/doc/images/app_color_picker.png +0 -0
  51. data/doc/images/app_debugging_showcase.gif +0 -0
  52. data/doc/images/app_debugging_showcase.png +0 -0
  53. data/doc/images/app_external_editor.gif +0 -0
  54. data/doc/images/app_login_form.png +0 -0
  55. data/doc/images/app_stateful_interaction.png +0 -0
  56. data/doc/images/verify_quickstart_dsl.png +0 -0
  57. data/doc/images/verify_quickstart_layout.png +0 -0
  58. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  59. data/doc/images/verify_readme_usage.png +0 -0
  60. data/doc/images/widget_barchart.png +0 -0
  61. data/doc/images/widget_block.png +0 -0
  62. data/doc/images/widget_box.png +0 -0
  63. data/doc/images/widget_calendar.png +0 -0
  64. data/doc/images/widget_canvas.png +0 -0
  65. data/doc/images/widget_cell.png +0 -0
  66. data/doc/images/widget_center.png +0 -0
  67. data/doc/images/widget_chart.png +0 -0
  68. data/doc/images/widget_gauge.png +0 -0
  69. data/doc/images/widget_layout_split.png +0 -0
  70. data/doc/images/widget_line_gauge.png +0 -0
  71. data/doc/images/widget_list.png +0 -0
  72. data/doc/images/widget_map.png +0 -0
  73. data/doc/images/widget_overlay.png +0 -0
  74. data/doc/images/widget_popup.png +0 -0
  75. data/doc/images/widget_ratatui_logo.png +0 -0
  76. data/doc/images/widget_ratatui_mascot.png +0 -0
  77. data/doc/images/widget_rect.png +0 -0
  78. data/doc/images/widget_render.png +0 -0
  79. data/doc/images/widget_rich_text.png +0 -0
  80. data/doc/images/widget_scroll_text.png +0 -0
  81. data/doc/images/widget_scrollbar.png +0 -0
  82. data/doc/images/widget_sparkline.png +0 -0
  83. data/doc/images/widget_style_colors.png +0 -0
  84. data/doc/images/widget_table.png +0 -0
  85. data/doc/images/widget_tabs.png +0 -0
  86. data/doc/images/widget_text_width.png +0 -0
  87. data/doc/index.md +0 -34
  88. data/doc/troubleshooting/async.md +0 -4
  89. data/doc/troubleshooting/terminal_limitations.md +0 -131
  90. data/doc/troubleshooting/tui_output.md +0 -197
  91. data/examples/app_all_events/README.md +0 -114
  92. data/examples/app_all_events/app.rb +0 -98
  93. data/examples/app_all_events/model/app_model.rb +0 -159
  94. data/examples/app_all_events/model/event_color_cycle.rb +0 -43
  95. data/examples/app_all_events/model/event_entry.rb +0 -94
  96. data/examples/app_all_events/model/msg.rb +0 -39
  97. data/examples/app_all_events/model/timestamp.rb +0 -56
  98. data/examples/app_all_events/update.rb +0 -75
  99. data/examples/app_all_events/view/app_view.rb +0 -80
  100. data/examples/app_all_events/view/controls_view.rb +0 -54
  101. data/examples/app_all_events/view/counts_view.rb +0 -61
  102. data/examples/app_all_events/view/live_view.rb +0 -72
  103. data/examples/app_all_events/view/log_view.rb +0 -57
  104. data/examples/app_all_events/view.rb +0 -9
  105. data/examples/app_cli_rich_moments/README.md +0 -81
  106. data/examples/app_cli_rich_moments/app.rb +0 -189
  107. data/examples/app_color_picker/README.md +0 -156
  108. data/examples/app_color_picker/app.rb +0 -76
  109. data/examples/app_color_picker/clipboard.rb +0 -86
  110. data/examples/app_color_picker/color.rb +0 -193
  111. data/examples/app_color_picker/controls.rb +0 -92
  112. data/examples/app_color_picker/copy_dialog.rb +0 -168
  113. data/examples/app_color_picker/export_pane.rb +0 -128
  114. data/examples/app_color_picker/harmony.rb +0 -58
  115. data/examples/app_color_picker/input.rb +0 -176
  116. data/examples/app_color_picker/main_container.rb +0 -180
  117. data/examples/app_color_picker/palette.rb +0 -111
  118. data/examples/app_debugging_showcase/README.md +0 -119
  119. data/examples/app_debugging_showcase/app.rb +0 -318
  120. data/examples/app_external_editor/README.md +0 -62
  121. data/examples/app_external_editor/app.rb +0 -344
  122. data/examples/app_login_form/README.md +0 -58
  123. data/examples/app_login_form/app.rb +0 -109
  124. data/examples/app_stateful_interaction/README.md +0 -35
  125. data/examples/app_stateful_interaction/app.rb +0 -328
  126. data/examples/timeout_demo.rb +0 -45
  127. data/examples/verify_quickstart_dsl/README.md +0 -55
  128. data/examples/verify_quickstart_dsl/app.rb +0 -49
  129. data/examples/verify_quickstart_layout/README.md +0 -77
  130. data/examples/verify_quickstart_layout/app.rb +0 -73
  131. data/examples/verify_quickstart_lifecycle/README.md +0 -68
  132. data/examples/verify_quickstart_lifecycle/app.rb +0 -62
  133. data/examples/verify_readme_usage/README.md +0 -49
  134. data/examples/verify_readme_usage/app.rb +0 -42
  135. data/examples/verify_website_managed/README.md +0 -48
  136. data/examples/verify_website_managed/app.rb +0 -36
  137. data/examples/verify_website_menu/README.md +0 -60
  138. data/examples/verify_website_menu/app.rb +0 -84
  139. data/examples/verify_website_spinner/README.md +0 -44
  140. data/examples/verify_website_spinner/app.rb +0 -34
  141. data/examples/widget_barchart/README.md +0 -58
  142. data/examples/widget_barchart/app.rb +0 -240
  143. data/examples/widget_block/README.md +0 -44
  144. data/examples/widget_block/app.rb +0 -258
  145. data/examples/widget_box/README.md +0 -54
  146. data/examples/widget_box/app.rb +0 -255
  147. data/examples/widget_calendar/README.md +0 -48
  148. data/examples/widget_calendar/app.rb +0 -115
  149. data/examples/widget_canvas/README.md +0 -31
  150. data/examples/widget_canvas/app.rb +0 -130
  151. data/examples/widget_cell/README.md +0 -45
  152. data/examples/widget_cell/app.rb +0 -112
  153. data/examples/widget_center/README.md +0 -33
  154. data/examples/widget_center/app.rb +0 -118
  155. data/examples/widget_chart/README.md +0 -50
  156. data/examples/widget_chart/app.rb +0 -220
  157. data/examples/widget_gauge/README.md +0 -50
  158. data/examples/widget_gauge/app.rb +0 -229
  159. data/examples/widget_layout_split/README.md +0 -53
  160. data/examples/widget_layout_split/app.rb +0 -260
  161. data/examples/widget_line_gauge/README.md +0 -50
  162. data/examples/widget_line_gauge/app.rb +0 -219
  163. data/examples/widget_list/README.md +0 -58
  164. data/examples/widget_list/app.rb +0 -382
  165. data/examples/widget_map/README.md +0 -48
  166. data/examples/widget_map/app.rb +0 -95
  167. data/examples/widget_overlay/README.md +0 -45
  168. data/examples/widget_overlay/app.rb +0 -250
  169. data/examples/widget_popup/README.md +0 -45
  170. data/examples/widget_popup/app.rb +0 -106
  171. data/examples/widget_ratatui_logo/README.md +0 -43
  172. data/examples/widget_ratatui_logo/app.rb +0 -104
  173. data/examples/widget_ratatui_mascot/README.md +0 -43
  174. data/examples/widget_ratatui_mascot/app.rb +0 -95
  175. data/examples/widget_rect/README.md +0 -53
  176. data/examples/widget_rect/app.rb +0 -222
  177. data/examples/widget_render/README.md +0 -46
  178. data/examples/widget_render/app.rb +0 -186
  179. data/examples/widget_render/app.rbs +0 -41
  180. data/examples/widget_rich_text/README.md +0 -44
  181. data/examples/widget_rich_text/app.rb +0 -193
  182. data/examples/widget_scroll_text/README.md +0 -46
  183. data/examples/widget_scroll_text/app.rb +0 -109
  184. data/examples/widget_scrollbar/README.md +0 -46
  185. data/examples/widget_scrollbar/app.rb +0 -155
  186. data/examples/widget_sparkline/README.md +0 -51
  187. data/examples/widget_sparkline/app.rb +0 -277
  188. data/examples/widget_style_colors/README.md +0 -43
  189. data/examples/widget_style_colors/app.rb +0 -83
  190. data/examples/widget_table/README.md +0 -57
  191. data/examples/widget_table/app.rb +0 -285
  192. data/examples/widget_tabs/README.md +0 -50
  193. data/examples/widget_tabs/app.rb +0 -183
  194. data/examples/widget_text_width/README.md +0 -44
  195. data/examples/widget_text_width/app.rb +0 -117
  196. data/migrate_to_buffer.rb +0 -145
  197. data/mise.toml +0 -8
  198. data/tasks/autodoc/examples.rb +0 -87
  199. data/tasks/autodoc/member.rb +0 -58
  200. data/tasks/autodoc/name.rb +0 -21
  201. data/tasks/autodoc.rake +0 -21
  202. data/tasks/bump/bump_workflow.rb +0 -49
  203. data/tasks/bump/cargo_lockfile.rb +0 -21
  204. data/tasks/bump/changelog.rb +0 -104
  205. data/tasks/bump/header.rb +0 -32
  206. data/tasks/bump/history.rb +0 -32
  207. data/tasks/bump/links.rb +0 -69
  208. data/tasks/bump/manifest.rb +0 -33
  209. data/tasks/bump/patch_release.rb +0 -19
  210. data/tasks/bump/release_branch.rb +0 -17
  211. data/tasks/bump/release_from_trunk.rb +0 -49
  212. data/tasks/bump/repository.rb +0 -54
  213. data/tasks/bump/ruby_gem.rb +0 -29
  214. data/tasks/bump/sem_ver.rb +0 -44
  215. data/tasks/bump/unreleased_section.rb +0 -73
  216. data/tasks/bump.rake +0 -61
  217. data/tasks/doc/documentation.rb +0 -59
  218. data/tasks/doc/link/file_url.rb +0 -30
  219. data/tasks/doc/link/relative_path.rb +0 -61
  220. data/tasks/doc/link/web_url.rb +0 -55
  221. data/tasks/doc/link.rb +0 -52
  222. data/tasks/doc/link_audit.rb +0 -116
  223. data/tasks/doc/problem.rb +0 -40
  224. data/tasks/doc/source_file.rb +0 -93
  225. data/tasks/doc.rake +0 -905
  226. data/tasks/example_viewer.html.erb +0 -172
  227. data/tasks/extension.rake +0 -14
  228. data/tasks/license/headers_md.rb +0 -223
  229. data/tasks/license/headers_rb.rb +0 -210
  230. data/tasks/license/license_utils.rb +0 -130
  231. data/tasks/license/snippets_md.rb +0 -315
  232. data/tasks/license/snippets_rdoc.rb +0 -150
  233. data/tasks/license.rake +0 -91
  234. data/tasks/lint.rake +0 -170
  235. data/tasks/rbs_predicates/predicate_catalog.rb +0 -52
  236. data/tasks/rbs_predicates/predicate_tests.rb +0 -124
  237. data/tasks/rbs_predicates/rbs_signature.rb +0 -63
  238. data/tasks/rbs_predicates.rake +0 -31
  239. data/tasks/rdoc_config.rb +0 -29
  240. data/tasks/resources/build.yml.erb +0 -60
  241. data/tasks/resources/index.html.erb +0 -141
  242. data/tasks/resources/rubies.yml +0 -7
  243. data/tasks/sourcehut.rake +0 -122
  244. data/tasks/steep.rake +0 -11
  245. data/tasks/terminal_preview/app_screenshot.rb +0 -45
  246. data/tasks/terminal_preview/crash_report.rb +0 -54
  247. data/tasks/terminal_preview/example_app.rb +0 -27
  248. data/tasks/terminal_preview/launcher_script.rb +0 -48
  249. data/tasks/terminal_preview/preview_collection.rb +0 -60
  250. data/tasks/terminal_preview/preview_timing.rb +0 -24
  251. data/tasks/terminal_preview/safety_confirmation.rb +0 -58
  252. data/tasks/terminal_preview/saved_screenshot.rb +0 -56
  253. data/tasks/terminal_preview/system_appearance.rb +0 -13
  254. data/tasks/terminal_preview/terminal_window.rb +0 -138
  255. data/tasks/terminal_preview/window_id.rb +0 -16
  256. data/tasks/terminal_preview.rake +0 -30
  257. data/tasks/test.rake +0 -36
  258. data/tasks/website/index_page.rb +0 -30
  259. data/tasks/website/version.rb +0 -122
  260. data/tasks/website/version_menu.rb +0 -68
  261. data/tasks/website/versioned_documentation.rb +0 -83
  262. data/tasks/website/website.rb +0 -53
  263. data/tasks/website.rake +0 -28
@@ -1,159 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #--
4
- # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
- # SPDX-License-Identifier: AGPL-3.0-or-later
6
- #++
7
-
8
- require_relative "timestamp"
9
- require_relative "event_entry"
10
- require_relative "event_color_cycle"
11
-
12
- # Immutable application state for the Model-View-Update architecture.
13
- #
14
- # The Elm Architecture requires a single immutable Model. State changes return
15
- # a new Model instance. This consolidates all app state into one place.
16
- #
17
- # Use `AppModel.initial` to create the starting state, and `model.with(...)`
18
- # to create updated states.
19
- #
20
- # === Attributes
21
- #
22
- # [entries] Array of EventEntry objects (event log)
23
- # [focused] Boolean window focus state
24
- # [window_size] Array [width, height] of terminal dimensions
25
- # [lit_types] Hash mapping event types to Timestamp (for highlight expiry)
26
- # [none_count] Integer count of :none events (not logged)
27
- # [color_cycle_index] Integer index into EventColorCycle::COLORS
28
- #
29
- # === Example
30
- #
31
- # model = AppModel.initial
32
- # model.count(:key) #=> 0
33
- # model.focused #=> true
34
- class AppModel < Data.define(:entries, :focused, :window_size, :lit_types, :none_count, :color_cycle_index)
35
- # Highlight duration in milliseconds.
36
- HIGHLIGHT_DURATION_MS = 300
37
-
38
- # Creates the initial application state.
39
- #
40
- # === Example
41
- #
42
- # AppModel.initial #=> #<data AppModel entries=[] focused=true ...>
43
- def self.initial
44
- new(
45
- entries: [],
46
- focused: true,
47
- window_size: [80, 24],
48
- lit_types: {},
49
- none_count: 0,
50
- color_cycle_index: 0
51
- )
52
- end
53
-
54
- # Returns the count of events for a given type.
55
- #
56
- # [type] Symbol event type (:key, :mouse, :resize, :paste, :focus, :none)
57
- #
58
- # === Example
59
- #
60
- # model.count(:key) #=> 5
61
- def count(type)
62
- return none_count if type == :none
63
-
64
- entries.count { |e| e.matches_type?(type) }
65
- end
66
-
67
- # Returns counts grouped by subtype (kind or modifier status).
68
- #
69
- # [type] Symbol event type.
70
- #
71
- # === Example
72
- #
73
- # model.sub_counts(:mouse) #=> { "down" => 1, "up" => 2 }
74
- def sub_counts(type)
75
- return {} if type == :none
76
-
77
- matching = entries.select { |e| e.matches_type?(type) }
78
- defaults = {
79
- key: %w[standard function media system modifier],
80
- focus: %w[gained lost],
81
- mouse: %w[down up drag moved scroll_up scroll_down],
82
- }
83
-
84
- matching.each_with_object(defaults.fetch(type, []).to_h { |k| [k, 0] }) do |entry, counts|
85
- group = subtype_for(entry, type)
86
- counts[group] += 1 if group
87
- end
88
- end
89
-
90
- # Checks if an event type should be highlighted.
91
- #
92
- # [type] Symbol event type.
93
- #
94
- # === Example
95
- #
96
- # model.lit?(:key) #=> true
97
- def lit?(type)
98
- timestamp = lit_types[type]
99
- return false unless timestamp
100
-
101
- !timestamp.elapsed?(HIGHLIGHT_DURATION_MS)
102
- end
103
-
104
- # Returns the most recent entries up to the given limit.
105
- #
106
- # [max_entries] Integer maximum number of entries to return.
107
- #
108
- # === Example
109
- #
110
- # model.visible(10) #=> [#<EventEntry ...>, ...]
111
- def visible(max_entries)
112
- entries.last(max_entries)
113
- end
114
-
115
- # Checks if any events have been recorded.
116
- #
117
- # === Example
118
- #
119
- # model.empty? #=> true
120
- def empty?
121
- entries.empty?
122
- end
123
-
124
- # Returns the most recent live event data for a type.
125
- #
126
- # [type] Symbol event type.
127
- #
128
- # === Example
129
- #
130
- # model.live_event(:key) #=> { time: Time, description: "..." }
131
- def live_event(type)
132
- entry = entries.reverse.find { |e| e.live_type == type }
133
- return nil unless entry
134
-
135
- { time: Time.at(entry.timestamp.milliseconds / 1000.0), description: entry.description }
136
- end
137
-
138
- # Returns the next color in the cycle for a new event.
139
- #
140
- # === Example
141
- #
142
- # model.next_color #=> :cyan
143
- def next_color
144
- EventColorCycle::COLORS[color_cycle_index]
145
- end
146
-
147
- private def subtype_for(entry, type)
148
- case type
149
- when :key
150
- # Key events: group by category kind (standard/function/media/modifier/system)
151
- entry.event.kind.to_s if entry.event.respond_to?(:kind)
152
- when :mouse
153
- # Mouse events: group by event kind (down/up/drag/moved/scroll_up/scroll_down)
154
- entry.event.kind.to_s if entry.event.respond_to?(:kind)
155
- when :focus
156
- entry.type.to_s.sub("focus_", "")
157
- end
158
- end
159
- end
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #--
4
- # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
- # SPDX-License-Identifier: AGPL-3.0-or-later
6
- #++
7
-
8
- # Cycles through a set of colors for event logging.
9
- #
10
- # Sequential events in a log are hard to distinguish if they all look the same.
11
- # Manually assigning colors to every event type or entry is repetitive.
12
- #
13
- # This class automatically cycles through a predefined list of vibrant colors.
14
- #
15
- # Use it to give each event in a log a distinct visual identity.
16
- #
17
- # === Examples
18
- #
19
- # cycler = EventColorCycle.new
20
- # cycler.next_color #=> :cyan
21
- # cycler.next_color #=> :magenta
22
- # cycler.next_color #=> :yellow
23
- # cycler.next_color #=> :cyan
24
- class EventColorCycle
25
- # List of colors to cycle through.
26
- COLORS = %i[cyan magenta yellow].freeze
27
-
28
- # Creates a new EventColorCycle.
29
- def initialize
30
- @index = 0
31
- end
32
-
33
- # Returns the next color in the cycle.
34
- #
35
- # === Example
36
- #
37
- # cycler.next_color #=> :cyan
38
- def next_color
39
- color = COLORS[@index]
40
- @index = (@index + 1) % COLORS.length
41
- color
42
- end
43
- end
@@ -1,94 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #--
4
- # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
- # SPDX-License-Identifier: AGPL-3.0-or-later
6
- #++
7
-
8
- require_relative "timestamp"
9
- require "ratatui_ruby"
10
-
11
- # Stores details about a single event in the history log.
12
- #
13
- # Event logs need to store diverse data including types, keys, colors, and timestamps.
14
- # Managing loose hashes or arrays for event history is error-prone and hard to query.
15
- #
16
- # This class provides a structured data object for every recorded event.
17
- #
18
- # Use it to represent mouse clicks, key presses, or resize events in a log.
19
- #
20
- # === Examples
21
- #
22
- # # Typically created via Events.record
23
- # entry = EventEntry.create(key_event, :cyan, Timestamp.now)
24
- # puts entry.type #=> :key
25
- # puts entry.description #=> '#<RatatuiRuby::Event::Key ...>'
26
- class EventEntry < Data.define(:event, :color, :timestamp)
27
- # Creates a new EventEntry.
28
- #
29
- # [event] RatatuiRuby::Event object.
30
- # [color] Symbol color for the log display.
31
- # [timestamp] Timestamp of when the event occurred.
32
- def self.create(event, color, timestamp)
33
- new(
34
- event:,
35
- color:,
36
- timestamp:
37
- )
38
- end
39
-
40
- # Returns the event type.
41
- #
42
- # === Example
43
- #
44
- # entry.type #=> :key
45
- def type
46
- case event
47
- when RatatuiRuby::Event::Key then :key
48
- when RatatuiRuby::Event::Mouse then :mouse
49
- when RatatuiRuby::Event::Resize then :resize
50
- when RatatuiRuby::Event::Paste then :paste
51
- when RatatuiRuby::Event::FocusGained then :focus_gained
52
- when RatatuiRuby::Event::FocusLost then :focus_lost
53
- else :unknown
54
- end
55
- end
56
-
57
- # Returns the event description using inspect.
58
- #
59
- # === Example
60
- #
61
- # entry.description #=> '#<RatatuiRuby::Event::Key code="a" modifiers=[]>'
62
- def description
63
- event.inspect
64
- end
65
-
66
- # Checks if the entry matches the given type.
67
- #
68
- # [check_type] Symbol type to check against.
69
- #
70
- # === Example
71
- #
72
- # entry.matches_type?(:key) #=> true
73
- def matches_type?(check_type)
74
- return true if check_type == :focus && (type == :focus_gained || type == :focus_lost)
75
-
76
- type == check_type
77
- end
78
-
79
- # Returns the display type for live event grouping.
80
- #
81
- # Normalizes focus_gained and focus_lost to :focus.
82
- #
83
- # === Example
84
- #
85
- # entry.live_type #=> :focus
86
- def live_type
87
- case type
88
- when :focus_gained, :focus_lost
89
- :focus
90
- else
91
- type
92
- end
93
- end
94
- end
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #--
4
- # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
- # SPDX-License-Identifier: AGPL-3.0-or-later
6
- #++
7
-
8
- # Semantic message types for the Model-View-Update architecture.
9
- #
10
- # Raw events from the terminal are converted to semantic Msg types. This
11
- # decouples the Update function from the event system, making it easier
12
- # to test and reason about.
13
- #
14
- # === Example
15
- #
16
- # msg = Msg::Input.new(event: key_event)
17
- # msg = Msg::Quit.new
18
- module Msg
19
- # A keyboard, mouse, or paste event to record.
20
- Input = Data.define(:event)
21
-
22
- # A terminal resize event.
23
- #
24
- # [width] Integer new terminal width
25
- # [height] Integer new terminal height
26
- # [previous_size] Array [width, height] before resize
27
- Resize = Data.define(:width, :height, :previous_size)
28
-
29
- # A focus change event.
30
- #
31
- # [gained] Boolean true if focus was gained, false if lost
32
- Focus = Data.define(:gained)
33
-
34
- # A none/timeout event (no input received).
35
- NoneEvent = Data.define
36
-
37
- # A quit signal.
38
- Quit = Data.define
39
- end
@@ -1,56 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #--
4
- # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
- # SPDX-License-Identifier: AGPL-3.0-or-later
6
- #++
7
-
8
- # Represents a high-resolution point in time.
9
- #
10
- # Comparing events and calculating durations requires consistent time measurement.
11
- # Standard Time objects are often too granular or complex for simple millisecond offsets.
12
- #
13
- # This class provides a millisecond-precision timestamp for event measurement.
14
- #
15
- # Use it to track event timing, calculate elapsed time, or trigger debouncing.
16
- #
17
- # === Examples
18
- #
19
- # timestamp = Timestamp.now
20
- # puts timestamp.milliseconds
21
- #
22
- # if timestamp.elapsed?(300)
23
- # puts "More than 300ms have passed."
24
- # end
25
- class Timestamp < Data.define(:milliseconds)
26
- # Returns a new Timestamp representing the current time.
27
- #
28
- # === Example
29
- #
30
- # Timestamp.now #=> #<struct Timestamp milliseconds=123456789>
31
- def self.now
32
- new(milliseconds: (Time.now.to_f * 1000).to_i)
33
- end
34
-
35
- # Checks if a duration has passed since this timestamp.
36
- #
37
- # [duration_ms] Integer duration in milliseconds.
38
- #
39
- # === Example
40
- #
41
- # timestamp = Timestamp.now
42
- # sleep(0.5)
43
- # timestamp.elapsed?(300) #=> true
44
- def elapsed?(duration_ms)
45
- Timestamp.now.milliseconds >= milliseconds + duration_ms
46
- end
47
-
48
- # Returns the current time in milliseconds.
49
- #
50
- # === Example
51
- #
52
- # Timestamp.current #=> 123456789
53
- def self.current
54
- now.milliseconds
55
- end
56
- end
@@ -1,75 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #--
4
- # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
- # SPDX-License-Identifier: AGPL-3.0-or-later
6
- #++
7
-
8
- require_relative "model/app_model"
9
- require_relative "model/msg"
10
- require_relative "model/event_entry"
11
- require_relative "model/timestamp"
12
- require_relative "model/event_color_cycle"
13
-
14
- # Pure update function for the Model-View-Update architecture.
15
- #
16
- # Given a Msg and the current AppModel, returns the next AppModel.
17
- # This function is pure: it does not mutate arguments, draw to the screen,
18
- # or perform IO. It simply calculates the next state.
19
- #
20
- # === Example
21
- #
22
- # model = AppModel.initial
23
- # msg = Msg::Input.new(event: key_event)
24
- # new_model = Update.call(msg, model)
25
- module Update
26
- extend self
27
-
28
- # Processes a message and returns the next model.
29
- #
30
- # [msg] A Msg value object
31
- # [model] The current AppModel
32
- #
33
- # === Example
34
- #
35
- # Update.call(Msg::Quit.new, model) #=> model (unchanged)
36
- def call(msg, model)
37
- case msg
38
- in Msg::Quit
39
- model
40
- in Msg::NoneEvent
41
- model.with(none_count: model.none_count + 1)
42
- in Msg::Focus(gained:)
43
- event = gained ? RatatuiRuby::Event::FocusGained.new : RatatuiRuby::Event::FocusLost.new
44
- entry = create_entry(event, model)
45
- add_entry(model, entry, :focus).with(focused: gained)
46
- in Msg::Resize(width:, height:, previous_size: _)
47
- event = RatatuiRuby::Event::Resize.new(width:, height:)
48
- entry = create_entry(event, model)
49
- add_entry(model, entry, :resize).with(window_size: [width, height])
50
- in Msg::Input(event:)
51
- entry = create_entry(event, model)
52
- add_entry(model, entry, entry.live_type)
53
- else
54
- model
55
- end
56
- end
57
-
58
- # Creates an EventEntry with the next color and current timestamp.
59
- def create_entry(event, model)
60
- EventEntry.create(event, model.next_color, Timestamp.now)
61
- end
62
-
63
- # Adds an entry to the model, updates highlights, and advances the color cycle.
64
- def add_entry(model, entry, live_type)
65
- new_entries = model.entries + [entry]
66
- new_lit_types = model.lit_types.merge(live_type => Timestamp.now)
67
- new_color_index = (model.color_cycle_index + 1) % EventColorCycle::COLORS.length
68
-
69
- model.with(
70
- entries: new_entries,
71
- lit_types: new_lit_types,
72
- color_cycle_index: new_color_index
73
- )
74
- end
75
- end
@@ -1,80 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #--
4
- # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
- # SPDX-License-Identifier: AGPL-3.0-or-later
6
- #++
7
-
8
- require_relative "../view"
9
- require_relative "counts_view"
10
- require_relative "live_view"
11
- require_relative "log_view"
12
- require_relative "controls_view"
13
-
14
- # Orchestrates the complete UI layout and sub-view composition.
15
- #
16
- # Complex applications need a structured way to divide the screen and delegate rendering.
17
- # Placing all layout logic in one monolithic method makes the code difficult to maintain.
18
- #
19
- # This class defines the screen layout using a series of split constraints and delegates to sub-views.
20
- #
21
- # Use it as the root view for the All Events example application.
22
- #
23
- # === Examples
24
- #
25
- # app_view = View::App.new
26
- # app_view.call(model, tui, frame, area)
27
- class View::App
28
- # Creates a new View::App and initializes sub-views.
29
- def initialize
30
- @counts_view = View::Counts.new
31
- @live_view = View::Live.new
32
- @log_view = View::Log.new
33
- @controls_view = View::Controls.new
34
- end
35
-
36
- # Renders the entire application UI to the given area.
37
- #
38
- # [model] AppModel containing all application data.
39
- # [tui] RatatuiRuby instance.
40
- # [frame] RatatuiRuby::Frame being rendered.
41
- # [area] RatatuiRuby::Layout::Rect defining the total available space.
42
- #
43
- # === Example
44
- #
45
- # app_view.call(model, tui, frame, area)
46
- def call(model, tui, frame, area)
47
- main_area, control_area = tui.layout_split(
48
- area,
49
- direction: :vertical,
50
- constraints: [
51
- tui.constraint_fill(1),
52
- tui.constraint_length(3),
53
- ]
54
- )
55
-
56
- counts_area, _margin_area, right_area = tui.layout_split(
57
- main_area,
58
- direction: :horizontal,
59
- constraints: [
60
- tui.constraint_length(20),
61
- tui.constraint_length(1),
62
- tui.constraint_fill(1),
63
- ]
64
- )
65
-
66
- live_area, log_area = tui.layout_split(
67
- right_area,
68
- direction: :vertical,
69
- constraints: [
70
- tui.constraint_length(9),
71
- tui.constraint_fill(1),
72
- ]
73
- )
74
-
75
- @counts_view.call(model, tui, frame, counts_area)
76
- @live_view.call(model, tui, frame, live_area)
77
- @log_view.call(model, tui, frame, log_area)
78
- @controls_view.call(model, tui, frame, control_area)
79
- end
80
- end
@@ -1,54 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #--
4
- # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
- # SPDX-License-Identifier: AGPL-3.0-or-later
6
- #++
7
-
8
- require_relative "../view"
9
-
10
- # Renders the keyboard controls and shortcuts panel.
11
- #
12
- # Users need to know how to interact with the application and exit.
13
- # Hardcoding control descriptions into the main layout makes the code hard to read.
14
- #
15
- # This component renders a formatted paragraph listing available global shortcuts.
16
- #
17
- # Use it to display help information in a sidebar or dedicated panel.
18
- #
19
- # === Examples
20
- #
21
- # controls = View::Controls.new
22
- # controls.call(model, tui, frame, area)
23
- class View::Controls
24
- # Renders the controls widget to the given area.
25
- #
26
- # [model] AppModel (unused, included for consistent interface).
27
- # [tui] RatatuiRuby instance.
28
- # [frame] RatatuiRuby::Frame being rendered.
29
- # [area] RatatuiRuby::Layout::Rect defining the widget's bounds.
30
- #
31
- # === Example
32
- #
33
- # controls.call(model, tui, frame, area)
34
- def call(_model, tui, frame, area)
35
- hotkey_style = tui.style(modifiers: [:bold, :underlined])
36
-
37
- widget = tui.paragraph(
38
- text: [
39
- tui.text_line(spans: [
40
- tui.text_span(content: "q", style: hotkey_style),
41
- tui.text_span(content: ": Quit "),
42
- tui.text_span(content: "Ctrl+C", style: hotkey_style),
43
- tui.text_span(content: ": Quit"),
44
- ]),
45
- ],
46
- block: tui.block(
47
- title: "Controls",
48
- borders: [:all],
49
- border_style: tui.style(fg: :white)
50
- )
51
- )
52
- frame.render_widget(widget, area)
53
- end
54
- end
@@ -1,61 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #--
4
- # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
- # SPDX-License-Identifier: AGPL-3.0-or-later
6
- #++
7
-
8
- require_relative "../view"
9
-
10
- # Renders the event statistics dashboard.
11
- #
12
- # Developers auditing input need to see real-time counts of various event types.
13
- #
14
- # This component displays a list of event types with their total counts.
15
- #
16
- # Use it to build an interactive dashboard of application activity.
17
- class View::Counts
18
- # Renders the event counts widget to the given area.
19
- #
20
- # [model] AppModel containing event data.
21
- # [tui] RatatuiRuby instance.
22
- # [frame] RatatuiRuby::Frame being rendered.
23
- # [area] RatatuiRuby::Layout::Rect defining the widget's bounds.
24
- def call(model, tui, frame, area)
25
- dimmed_style = tui.style(fg: :dark_gray)
26
- lit_style = tui.style(fg: :green, modifiers: [:bold])
27
- border_color = model.focused ? :green : :gray
28
-
29
- count_lines = []
30
-
31
- AppAllEvents::EVENT_TYPES.each do |type|
32
- count = model.count(type)
33
- label = type.to_s.capitalize
34
- style = model.lit?(type) ? lit_style : nil
35
-
36
- count_lines << tui.text_line(spans: [
37
- tui.text_span(content: "#{label}: ", style:),
38
- tui.text_span(content: count.to_s, style: style || tui.style(fg: :yellow)),
39
- ])
40
-
41
- model.sub_counts(type).each do |sub_type, sub_count|
42
- sub_label = sub_type.to_s.capitalize
43
- count_lines << tui.text_line(spans: [
44
- tui.text_span(content: " #{sub_label}: ", style: dimmed_style),
45
- tui.text_span(content: sub_count.to_s, style: dimmed_style),
46
- ])
47
- end
48
- end
49
-
50
- widget = tui.paragraph(
51
- text: count_lines,
52
- scroll: [0, 0],
53
- block: tui.block(
54
- title: "Event Counts",
55
- borders: [:all],
56
- border_style: tui.style(fg: border_color)
57
- )
58
- )
59
- frame.render_widget(widget, area)
60
- end
61
- end