ratatui_ruby 0.5.0 → 0.6.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 (234) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +1 -1
  3. data/.builds/ruby-3.3.yml +1 -1
  4. data/.builds/ruby-3.4.yml +1 -1
  5. data/.builds/ruby-4.0.0.yml +1 -1
  6. data/AGENTS.md +6 -0
  7. data/CHANGELOG.md +44 -7
  8. data/README.md +11 -4
  9. data/REUSE.toml +2 -7
  10. data/doc/application_architecture.md +84 -10
  11. data/doc/application_testing.md +75 -29
  12. data/doc/contributors/design/ruby_frontend.md +39 -3
  13. data/doc/contributors/design/rust_backend.md +1 -0
  14. data/doc/contributors/developing_examples.md +129 -44
  15. data/doc/contributors/examples_audit/p1_high.md +21 -0
  16. data/doc/contributors/examples_audit/p2_moderate.md +81 -0
  17. data/doc/contributors/examples_audit.md +41 -0
  18. data/doc/event_handling.md +11 -3
  19. data/doc/images/app_all_events.png +0 -0
  20. data/doc/images/app_color_picker.png +0 -0
  21. data/doc/images/app_login_form.png +0 -0
  22. data/doc/images/app_stateful_interaction.png +0 -0
  23. data/doc/images/verify_quickstart_dsl.png +0 -0
  24. data/doc/images/verify_quickstart_layout.png +0 -0
  25. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  26. data/doc/images/verify_readme_usage.png +0 -0
  27. data/doc/images/widget_barchart_demo.png +0 -0
  28. data/doc/images/widget_block_demo.png +0 -0
  29. data/doc/images/widget_canvas_demo.png +0 -0
  30. data/doc/images/widget_cell_demo.png +0 -0
  31. data/doc/images/widget_center_demo.png +0 -0
  32. data/doc/images/widget_chart_demo.png +0 -0
  33. data/doc/images/widget_list_demo.png +0 -0
  34. data/doc/images/widget_overlay_demo.png +0 -0
  35. data/doc/images/widget_render.png +0 -0
  36. data/doc/images/widget_rich_text.png +0 -0
  37. data/doc/images/widget_scroll_text.png +0 -0
  38. data/doc/images/widget_sparkline_demo.png +0 -0
  39. data/doc/images/widget_table_demo.png +0 -0
  40. data/doc/images/widget_tabs_demo.png +0 -0
  41. data/doc/images/widget_text_width.png +0 -0
  42. data/doc/quickstart.md +69 -76
  43. data/doc/terminal_limitations.md +92 -0
  44. data/examples/app_all_events/README.md +45 -27
  45. data/examples/app_all_events/app.rb +38 -35
  46. data/examples/app_all_events/model/app_model.rb +157 -0
  47. data/examples/app_all_events/model/event_entry.rb +17 -0
  48. data/examples/app_all_events/model/msg.rb +37 -0
  49. data/examples/app_all_events/update.rb +73 -0
  50. data/examples/app_all_events/view/app_view.rb +8 -8
  51. data/examples/app_all_events/view/controls_view.rb +8 -6
  52. data/examples/app_all_events/view/counts_view.rb +12 -8
  53. data/examples/app_all_events/view/live_view.rb +8 -7
  54. data/examples/app_all_events/view/log_view.rb +10 -15
  55. data/examples/app_color_picker/README.md +84 -44
  56. data/examples/app_color_picker/app.rb +24 -62
  57. data/examples/app_color_picker/controls.rb +90 -0
  58. data/examples/app_color_picker/copy_dialog.rb +45 -49
  59. data/examples/app_color_picker/export_pane.rb +126 -0
  60. data/examples/app_color_picker/input.rb +99 -67
  61. data/examples/app_color_picker/main_container.rb +178 -0
  62. data/examples/app_color_picker/palette.rb +55 -26
  63. data/examples/app_login_form/README.md +47 -0
  64. data/examples/app_login_form/app.rb +2 -3
  65. data/examples/app_stateful_interaction/README.md +31 -0
  66. data/examples/app_stateful_interaction/app.rb +272 -0
  67. data/examples/timeout_demo.rb +43 -0
  68. data/examples/verify_quickstart_dsl/README.md +48 -0
  69. data/examples/verify_quickstart_dsl/app.rb +2 -0
  70. data/examples/verify_quickstart_layout/README.md +71 -0
  71. data/examples/verify_quickstart_layout/app.rb +2 -0
  72. data/examples/verify_quickstart_lifecycle/README.md +56 -0
  73. data/examples/verify_quickstart_lifecycle/app.rb +8 -2
  74. data/examples/verify_readme_usage/README.md +43 -0
  75. data/examples/verify_readme_usage/app.rb +8 -2
  76. data/examples/widget_barchart_demo/README.md +49 -0
  77. data/examples/widget_barchart_demo/app.rb +5 -5
  78. data/examples/widget_block_demo/README.md +34 -0
  79. data/examples/widget_block_demo/app.rb +256 -0
  80. data/examples/widget_box_demo/README.md +45 -0
  81. data/examples/widget_calendar_demo/README.md +39 -0
  82. data/examples/widget_canvas_demo/README.md +27 -0
  83. data/examples/widget_canvas_demo/app.rb +123 -0
  84. data/examples/widget_cell_demo/README.md +36 -0
  85. data/examples/widget_cell_demo/app.rb +31 -24
  86. data/examples/widget_center_demo/README.md +29 -0
  87. data/examples/widget_center_demo/app.rb +116 -0
  88. data/examples/widget_chart_demo/README.md +41 -0
  89. data/examples/widget_chart_demo/app.rb +7 -2
  90. data/examples/widget_gauge_demo/README.md +41 -0
  91. data/examples/widget_layout_split/README.md +44 -0
  92. data/examples/widget_line_gauge_demo/README.md +41 -0
  93. data/examples/widget_list_demo/README.md +49 -0
  94. data/examples/widget_list_demo/app.rb +91 -107
  95. data/examples/widget_map_demo/README.md +39 -0
  96. data/examples/{app_map_demo → widget_map_demo}/app.rb +2 -2
  97. data/examples/widget_overlay_demo/app.rb +248 -0
  98. data/examples/widget_popup_demo/README.md +36 -0
  99. data/examples/widget_ratatui_logo_demo/README.md +34 -0
  100. data/examples/widget_ratatui_mascot_demo/README.md +34 -0
  101. data/examples/widget_rect/README.md +38 -0
  102. data/examples/widget_render/README.md +37 -0
  103. data/examples/widget_rich_text/README.md +35 -0
  104. data/examples/widget_rich_text/app.rb +62 -33
  105. data/examples/widget_scroll_text/README.md +37 -0
  106. data/examples/widget_scroll_text/app.rb +0 -1
  107. data/examples/widget_scrollbar_demo/README.md +37 -0
  108. data/examples/widget_sparkline_demo/README.md +42 -0
  109. data/examples/widget_sparkline_demo/app.rb +4 -3
  110. data/examples/widget_style_colors/README.md +34 -0
  111. data/examples/widget_table_demo/README.md +48 -0
  112. data/examples/{app_table_select → widget_table_demo}/app.rb +46 -8
  113. data/examples/widget_tabs_demo/README.md +41 -0
  114. data/examples/widget_tabs_demo/app.rb +15 -1
  115. data/examples/widget_text_width/README.md +35 -0
  116. data/examples/widget_text_width/app.rb +106 -0
  117. data/exe/.gitkeep +0 -0
  118. data/ext/ratatui_ruby/Cargo.lock +11 -4
  119. data/ext/ratatui_ruby/Cargo.toml +2 -1
  120. data/ext/ratatui_ruby/src/events.rs +238 -26
  121. data/ext/ratatui_ruby/src/frame.rs +113 -1
  122. data/ext/ratatui_ruby/src/lib.rs +34 -4
  123. data/ext/ratatui_ruby/src/string_width.rs +101 -0
  124. data/ext/ratatui_ruby/src/terminal.rs +39 -15
  125. data/ext/ratatui_ruby/src/text.rs +1 -1
  126. data/ext/ratatui_ruby/src/widgets/barchart.rs +24 -6
  127. data/ext/ratatui_ruby/src/widgets/gauge.rs +9 -2
  128. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +9 -2
  129. data/ext/ratatui_ruby/src/widgets/list.rs +179 -3
  130. data/ext/ratatui_ruby/src/widgets/list_state.rs +137 -0
  131. data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
  132. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +93 -1
  133. data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +169 -0
  134. data/ext/ratatui_ruby/src/widgets/table.rs +113 -1
  135. data/ext/ratatui_ruby/src/widgets/table_state.rs +121 -0
  136. data/lib/ratatui_ruby/cell.rb +4 -4
  137. data/lib/ratatui_ruby/event/key/character.rb +35 -0
  138. data/lib/ratatui_ruby/event/key/media.rb +44 -0
  139. data/lib/ratatui_ruby/event/key/modifier.rb +95 -0
  140. data/lib/ratatui_ruby/event/key/navigation.rb +55 -0
  141. data/lib/ratatui_ruby/event/key/system.rb +45 -0
  142. data/lib/ratatui_ruby/event/key.rb +111 -51
  143. data/lib/ratatui_ruby/event/mouse.rb +3 -3
  144. data/lib/ratatui_ruby/event/paste.rb +1 -1
  145. data/lib/ratatui_ruby/frame.rb +96 -0
  146. data/lib/ratatui_ruby/list_state.rb +88 -0
  147. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +2 -2
  148. data/lib/ratatui_ruby/schema/cursor.rb +5 -0
  149. data/lib/ratatui_ruby/schema/gauge.rb +3 -1
  150. data/lib/ratatui_ruby/schema/line_gauge.rb +2 -2
  151. data/lib/ratatui_ruby/schema/list.rb +25 -4
  152. data/lib/ratatui_ruby/schema/list_item.rb +41 -0
  153. data/lib/ratatui_ruby/schema/rect.rb +43 -0
  154. data/lib/ratatui_ruby/schema/style.rb +24 -4
  155. data/lib/ratatui_ruby/schema/table.rb +21 -3
  156. data/lib/ratatui_ruby/schema/text.rb +69 -1
  157. data/lib/ratatui_ruby/scrollbar_state.rb +112 -0
  158. data/lib/ratatui_ruby/session/autodoc.rb +65 -0
  159. data/lib/ratatui_ruby/session.rb +22 -7
  160. data/lib/ratatui_ruby/table_state.rb +90 -0
  161. data/lib/ratatui_ruby/test_helper/event_injection.rb +169 -0
  162. data/lib/ratatui_ruby/test_helper/snapshot.rb +390 -0
  163. data/lib/ratatui_ruby/test_helper/style_assertions.rb +351 -0
  164. data/lib/ratatui_ruby/test_helper/terminal.rb +127 -0
  165. data/lib/ratatui_ruby/test_helper/test_doubles.rb +68 -0
  166. data/lib/ratatui_ruby/test_helper.rb +65 -358
  167. data/lib/ratatui_ruby/version.rb +1 -1
  168. data/lib/ratatui_ruby.rb +42 -19
  169. data/sig/examples/app_stateful_interaction/app.rbs +33 -0
  170. data/sig/examples/widget_block_demo/app.rbs +32 -0
  171. data/sig/examples/{app_map_demo → widget_map_demo}/app.rbs +2 -2
  172. data/sig/examples/{app_table_select → widget_table_demo}/app.rbs +2 -2
  173. data/sig/examples/{widget_table_flex → widget_text_width}/app.rbs +2 -3
  174. data/sig/ratatui_ruby/event.rbs +11 -1
  175. data/sig/ratatui_ruby/frame.rbs +2 -0
  176. data/sig/ratatui_ruby/list_state.rbs +13 -0
  177. data/sig/ratatui_ruby/ratatui_ruby.rbs +2 -2
  178. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +3 -3
  179. data/sig/ratatui_ruby/schema/gauge.rbs +2 -2
  180. data/sig/ratatui_ruby/schema/line_gauge.rbs +2 -2
  181. data/sig/ratatui_ruby/schema/list.rbs +4 -2
  182. data/sig/ratatui_ruby/schema/list_item.rbs +10 -0
  183. data/sig/ratatui_ruby/schema/rect.rbs +3 -0
  184. data/sig/ratatui_ruby/schema/style.rbs +3 -3
  185. data/sig/ratatui_ruby/schema/table.rbs +3 -1
  186. data/sig/ratatui_ruby/schema/text.rbs +8 -6
  187. data/sig/ratatui_ruby/scrollbar_state.rbs +18 -0
  188. data/sig/ratatui_ruby/session.rbs +13 -0
  189. data/sig/ratatui_ruby/table_state.rbs +15 -0
  190. data/sig/ratatui_ruby/test_helper/event_injection.rbs +16 -0
  191. data/sig/ratatui_ruby/test_helper/snapshot.rbs +12 -0
  192. data/sig/ratatui_ruby/test_helper/style_assertions.rbs +64 -0
  193. data/sig/ratatui_ruby/test_helper/terminal.rbs +14 -0
  194. data/sig/ratatui_ruby/test_helper/test_doubles.rbs +22 -0
  195. data/sig/ratatui_ruby/test_helper.rbs +5 -4
  196. data/tasks/autodoc/examples.rb +79 -0
  197. data/tasks/autodoc/inventory.rb +9 -7
  198. data/tasks/autodoc.rake +11 -5
  199. data/tasks/bump/changelog.rb +3 -3
  200. data/tasks/bump/links.rb +67 -0
  201. data/tasks/sourcehut.rake +61 -21
  202. data/tasks/terminal_preview/app_screenshot.rb +13 -3
  203. data/tasks/terminal_preview/saved_screenshot.rb +4 -3
  204. metadata +111 -37
  205. data/doc/images/app_table_select.png +0 -0
  206. data/doc/images/widget_block_padding.png +0 -0
  207. data/doc/images/widget_block_titles.png +0 -0
  208. data/doc/images/widget_list_styles.png +0 -0
  209. data/examples/app_all_events/model/events.rb +0 -180
  210. data/examples/app_all_events/model/highlight.rb +0 -57
  211. data/examples/app_all_events/test/snapshots/after_focus_lost.txt +0 -24
  212. data/examples/app_all_events/test/snapshots/after_focus_regained.txt +0 -24
  213. data/examples/app_all_events/test/snapshots/after_horizontal_resize.txt +0 -24
  214. data/examples/app_all_events/test/snapshots/after_key_a.txt +0 -24
  215. data/examples/app_all_events/test/snapshots/after_key_ctrl_x.txt +0 -24
  216. data/examples/app_all_events/test/snapshots/after_mouse_click.txt +0 -24
  217. data/examples/app_all_events/test/snapshots/after_mouse_drag.txt +0 -24
  218. data/examples/app_all_events/test/snapshots/after_multiple_events.txt +0 -24
  219. data/examples/app_all_events/test/snapshots/after_paste.txt +0 -24
  220. data/examples/app_all_events/test/snapshots/after_resize.txt +0 -24
  221. data/examples/app_all_events/test/snapshots/after_right_click.txt +0 -24
  222. data/examples/app_all_events/test/snapshots/after_vertical_resize.txt +0 -24
  223. data/examples/app_all_events/test/snapshots/initial_state.txt +0 -24
  224. data/examples/app_all_events/view_state.rb +0 -42
  225. data/examples/app_color_picker/scene.rb +0 -201
  226. data/examples/widget_block_padding/app.rb +0 -67
  227. data/examples/widget_block_titles/app.rb +0 -69
  228. data/examples/widget_list_styles/app.rb +0 -141
  229. data/examples/widget_table_flex/app.rb +0 -95
  230. data/sig/examples/widget_block_padding/app.rbs +0 -11
  231. data/sig/examples/widget_block_titles/app.rbs +0 -11
  232. data/sig/examples/widget_list_styles/app.rbs +0 -11
  233. data/tasks/bump/comparison_links.rb +0 -41
  234. /data/doc/images/{app_map_demo.png → widget_map_demo.png} +0 -0
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ require_relative "timestamp"
7
+ require_relative "event_entry"
8
+ require_relative "event_color_cycle"
9
+
10
+ # Immutable application state for the Proto-TEA architecture.
11
+ #
12
+ # The Elm Architecture requires a single immutable Model. State changes return
13
+ # a new Model instance. This consolidates all app state into one place.
14
+ #
15
+ # Use `AppModel.initial` to create the starting state, and `model.with(...)`
16
+ # to create updated states.
17
+ #
18
+ # === Attributes
19
+ #
20
+ # [entries] Array of EventEntry objects (event log)
21
+ # [focused] Boolean window focus state
22
+ # [window_size] Array [width, height] of terminal dimensions
23
+ # [lit_types] Hash mapping event types to Timestamp (for highlight expiry)
24
+ # [none_count] Integer count of :none events (not logged)
25
+ # [color_cycle_index] Integer index into EventColorCycle::COLORS
26
+ #
27
+ # === Example
28
+ #
29
+ # model = AppModel.initial
30
+ # model.count(:key) #=> 0
31
+ # model.focused #=> true
32
+ class AppModel < Data.define(:entries, :focused, :window_size, :lit_types, :none_count, :color_cycle_index)
33
+ # Highlight duration in milliseconds.
34
+ HIGHLIGHT_DURATION_MS = 300
35
+
36
+ # Creates the initial application state.
37
+ #
38
+ # === Example
39
+ #
40
+ # AppModel.initial #=> #<data AppModel entries=[] focused=true ...>
41
+ def self.initial
42
+ new(
43
+ entries: [],
44
+ focused: true,
45
+ window_size: [80, 24],
46
+ lit_types: {},
47
+ none_count: 0,
48
+ color_cycle_index: 0
49
+ )
50
+ end
51
+
52
+ # Returns the count of events for a given type.
53
+ #
54
+ # [type] Symbol event type (:key, :mouse, :resize, :paste, :focus, :none)
55
+ #
56
+ # === Example
57
+ #
58
+ # model.count(:key) #=> 5
59
+ def count(type)
60
+ return none_count if type == :none
61
+
62
+ entries.count { |e| e.matches_type?(type) }
63
+ end
64
+
65
+ # Returns counts grouped by subtype (kind or modifier status).
66
+ #
67
+ # [type] Symbol event type.
68
+ #
69
+ # === Example
70
+ #
71
+ # model.sub_counts(:mouse) #=> { "down" => 1, "up" => 2 }
72
+ def sub_counts(type)
73
+ return {} if type == :none
74
+
75
+ matching = entries.select { |e| e.matches_type?(type) }
76
+ defaults = {
77
+ key: %w[standard function media system modifier],
78
+ focus: %w[gained lost],
79
+ mouse: %w[down up drag moved scroll_up scroll_down],
80
+ }
81
+
82
+ matching.each_with_object(defaults.fetch(type, []).to_h { |k| [k, 0] }) do |entry, counts|
83
+ group = subtype_for(entry, type)
84
+ counts[group] += 1 if group
85
+ end
86
+ end
87
+
88
+ # Checks if an event type should be highlighted.
89
+ #
90
+ # [type] Symbol event type.
91
+ #
92
+ # === Example
93
+ #
94
+ # model.lit?(:key) #=> true
95
+ def lit?(type)
96
+ timestamp = lit_types[type]
97
+ return false unless timestamp
98
+
99
+ !timestamp.elapsed?(HIGHLIGHT_DURATION_MS)
100
+ end
101
+
102
+ # Returns the most recent entries up to the given limit.
103
+ #
104
+ # [max_entries] Integer maximum number of entries to return.
105
+ #
106
+ # === Example
107
+ #
108
+ # model.visible(10) #=> [#<EventEntry ...>, ...]
109
+ def visible(max_entries)
110
+ entries.last(max_entries)
111
+ end
112
+
113
+ # Checks if any events have been recorded.
114
+ #
115
+ # === Example
116
+ #
117
+ # model.empty? #=> true
118
+ def empty?
119
+ entries.empty?
120
+ end
121
+
122
+ # Returns the most recent live event data for a type.
123
+ #
124
+ # [type] Symbol event type.
125
+ #
126
+ # === Example
127
+ #
128
+ # model.live_event(:key) #=> { time: Time, description: "..." }
129
+ def live_event(type)
130
+ entry = entries.reverse.find { |e| e.live_type == type }
131
+ return nil unless entry
132
+
133
+ { time: Time.at(entry.timestamp.milliseconds / 1000.0), description: entry.description }
134
+ end
135
+
136
+ # Returns the next color in the cycle for a new event.
137
+ #
138
+ # === Example
139
+ #
140
+ # model.next_color #=> :cyan
141
+ def next_color
142
+ EventColorCycle::COLORS[color_cycle_index]
143
+ end
144
+
145
+ private def subtype_for(entry, type)
146
+ case type
147
+ when :key
148
+ # Key events: group by category kind (standard/function/media/modifier/system)
149
+ entry.event.kind.to_s if entry.event.respond_to?(:kind)
150
+ when :mouse
151
+ # Mouse events: group by event kind (down/up/drag/moved/scroll_up/scroll_down)
152
+ entry.event.kind.to_s if entry.event.respond_to?(:kind)
153
+ when :focus
154
+ entry.type.to_s.sub("focus_", "")
155
+ end
156
+ end
157
+ end
@@ -70,6 +70,23 @@ class EventEntry < Data.define(:event, :color, :timestamp)
70
70
  # entry.matches_type?(:key) #=> true
71
71
  def matches_type?(check_type)
72
72
  return true if check_type == :focus && (type == :focus_gained || type == :focus_lost)
73
+
73
74
  type == check_type
74
75
  end
76
+
77
+ # Returns the display type for live event grouping.
78
+ #
79
+ # Normalizes focus_gained and focus_lost to :focus.
80
+ #
81
+ # === Example
82
+ #
83
+ # entry.live_type #=> :focus
84
+ def live_type
85
+ case type
86
+ when :focus_gained, :focus_lost
87
+ :focus
88
+ else
89
+ type
90
+ end
91
+ end
75
92
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ # Semantic message types for the Proto-TEA architecture.
7
+ #
8
+ # Raw events from the terminal are converted to semantic Msg types. This
9
+ # decouples the Update function from the event system, making it easier
10
+ # to test and reason about.
11
+ #
12
+ # === Example
13
+ #
14
+ # msg = Msg::Input.new(event: key_event)
15
+ # msg = Msg::Quit.new
16
+ module Msg
17
+ # A keyboard, mouse, or paste event to record.
18
+ Input = Data.define(:event)
19
+
20
+ # A terminal resize event.
21
+ #
22
+ # [width] Integer new terminal width
23
+ # [height] Integer new terminal height
24
+ # [previous_size] Array [width, height] before resize
25
+ Resize = Data.define(:width, :height, :previous_size)
26
+
27
+ # A focus change event.
28
+ #
29
+ # [gained] Boolean true if focus was gained, false if lost
30
+ Focus = Data.define(:gained)
31
+
32
+ # A none/timeout event (no input received).
33
+ NoneEvent = Data.define
34
+
35
+ # A quit signal.
36
+ Quit = Data.define
37
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ require_relative "model/app_model"
7
+ require_relative "model/msg"
8
+ require_relative "model/event_entry"
9
+ require_relative "model/timestamp"
10
+ require_relative "model/event_color_cycle"
11
+
12
+ # Pure update function for the Proto-TEA architecture.
13
+ #
14
+ # Given a Msg and the current AppModel, returns the next AppModel.
15
+ # This function is pure: it does not mutate arguments, draw to the screen,
16
+ # or perform IO. It simply calculates the next state.
17
+ #
18
+ # === Example
19
+ #
20
+ # model = AppModel.initial
21
+ # msg = Msg::Input.new(event: key_event)
22
+ # new_model = Update.call(msg, model)
23
+ module Update
24
+ extend self
25
+
26
+ # Processes a message and returns the next model.
27
+ #
28
+ # [msg] A Msg value object
29
+ # [model] The current AppModel
30
+ #
31
+ # === Example
32
+ #
33
+ # Update.call(Msg::Quit.new, model) #=> model (unchanged)
34
+ def call(msg, model)
35
+ case msg
36
+ in Msg::Quit
37
+ model
38
+ in Msg::NoneEvent
39
+ model.with(none_count: model.none_count + 1)
40
+ in Msg::Focus(gained:)
41
+ event = gained ? RatatuiRuby::Event::FocusGained.new : RatatuiRuby::Event::FocusLost.new
42
+ entry = create_entry(event, model)
43
+ add_entry(model, entry, :focus).with(focused: gained)
44
+ in Msg::Resize(width:, height:, previous_size: _)
45
+ event = RatatuiRuby::Event::Resize.new(width:, height:)
46
+ entry = create_entry(event, model)
47
+ add_entry(model, entry, :resize).with(window_size: [width, height])
48
+ in Msg::Input(event:)
49
+ entry = create_entry(event, model)
50
+ add_entry(model, entry, entry.live_type)
51
+ else
52
+ model
53
+ end
54
+ end
55
+
56
+ # Creates an EventEntry with the next color and current timestamp.
57
+ def create_entry(event, model)
58
+ EventEntry.create(event, model.next_color, Timestamp.now)
59
+ end
60
+
61
+ # Adds an entry to the model, updates highlights, and advances the color cycle.
62
+ def add_entry(model, entry, live_type)
63
+ new_entries = model.entries + [entry]
64
+ new_lit_types = model.lit_types.merge(live_type => Timestamp.now)
65
+ new_color_index = (model.color_cycle_index + 1) % EventColorCycle::COLORS.length
66
+
67
+ model.with(
68
+ entries: new_entries,
69
+ lit_types: new_lit_types,
70
+ color_cycle_index: new_color_index
71
+ )
72
+ end
73
+ end
@@ -21,7 +21,7 @@ require_relative "controls_view"
21
21
  # === Examples
22
22
  #
23
23
  # app_view = View::App.new
24
- # app_view.call(state, tui, frame, area)
24
+ # app_view.call(model, tui, frame, area)
25
25
  class View::App
26
26
  # Creates a new View::App and initializes sub-views.
27
27
  def initialize
@@ -33,15 +33,15 @@ class View::App
33
33
 
34
34
  # Renders the entire application UI to the given area.
35
35
  #
36
- # [state] ViewState containing all application data.
36
+ # [model] AppModel containing all application data.
37
37
  # [tui] RatatuiRuby instance.
38
38
  # [frame] RatatuiRuby::Frame being rendered.
39
39
  # [area] RatatuiRuby::Rect defining the total available space.
40
40
  #
41
41
  # === Example
42
42
  #
43
- # app_view.call(state, tui, frame, area)
44
- def call(state, tui, frame, area)
43
+ # app_view.call(model, tui, frame, area)
44
+ def call(model, tui, frame, area)
45
45
  main_area, control_area = tui.layout_split(
46
46
  area,
47
47
  direction: :vertical,
@@ -70,9 +70,9 @@ class View::App
70
70
  ]
71
71
  )
72
72
 
73
- @counts_view.call(state, tui, frame, counts_area)
74
- @live_view.call(state, tui, frame, live_area)
75
- @log_view.call(state, tui, frame, log_area)
76
- @controls_view.call(state, tui, frame, control_area)
73
+ @counts_view.call(model, tui, frame, counts_area)
74
+ @live_view.call(model, tui, frame, live_area)
75
+ @log_view.call(model, tui, frame, log_area)
76
+ @controls_view.call(model, tui, frame, control_area)
77
77
  end
78
78
  end
@@ -17,25 +17,27 @@ require_relative "../view"
17
17
  # === Examples
18
18
  #
19
19
  # controls = View::Controls.new
20
- # controls.call(state, tui, frame, area)
20
+ # controls.call(model, tui, frame, area)
21
21
  class View::Controls
22
22
  # Renders the controls widget to the given area.
23
23
  #
24
- # [state] ViewState containing style information.
24
+ # [model] AppModel (unused, included for consistent interface).
25
25
  # [tui] RatatuiRuby instance.
26
26
  # [frame] RatatuiRuby::Frame being rendered.
27
27
  # [area] RatatuiRuby::Rect defining the widget's bounds.
28
28
  #
29
29
  # === Example
30
30
  #
31
- # controls.call(state, tui, frame, area)
32
- def call(state, tui, frame, area)
31
+ # controls.call(model, tui, frame, area)
32
+ def call(_model, tui, frame, area)
33
+ hotkey_style = tui.style(modifiers: [:bold, :underlined])
34
+
33
35
  widget = tui.paragraph(
34
36
  text: [
35
37
  tui.text_line(spans: [
36
- tui.text_span(content: "q", style: state.hotkey_style),
38
+ tui.text_span(content: "q", style: hotkey_style),
37
39
  tui.text_span(content: ": Quit "),
38
- tui.text_span(content: "Ctrl+C", style: state.hotkey_style),
40
+ tui.text_span(content: "Ctrl+C", style: hotkey_style),
39
41
  tui.text_span(content: ": Quit"),
40
42
  ]),
41
43
  ],
@@ -15,28 +15,32 @@ require_relative "../view"
15
15
  class View::Counts
16
16
  # Renders the event counts widget to the given area.
17
17
  #
18
- # [state] ViewState containing event data and styles.
18
+ # [model] AppModel containing event data.
19
19
  # [tui] RatatuiRuby instance.
20
20
  # [frame] RatatuiRuby::Frame being rendered.
21
21
  # [area] RatatuiRuby::Rect defining the widget's bounds.
22
- def call(state, tui, frame, area)
22
+ def call(model, tui, frame, area)
23
+ dimmed_style = tui.style(fg: :dark_gray)
24
+ lit_style = tui.style(fg: :green, modifiers: [:bold])
25
+ border_color = model.focused ? :green : :gray
26
+
23
27
  count_lines = []
24
28
 
25
29
  AppAllEvents::EVENT_TYPES.each do |type|
26
- count = state.events.count(type)
30
+ count = model.count(type)
27
31
  label = type.to_s.capitalize
28
- style = state.events.lit?(type) ? state.lit_style : nil
32
+ style = model.lit?(type) ? lit_style : nil
29
33
 
30
34
  count_lines << tui.text_line(spans: [
31
35
  tui.text_span(content: "#{label}: ", style:),
32
36
  tui.text_span(content: count.to_s, style: style || tui.style(fg: :yellow)),
33
37
  ])
34
38
 
35
- state.events.sub_counts(type).each do |sub_type, sub_count|
39
+ model.sub_counts(type).each do |sub_type, sub_count|
36
40
  sub_label = sub_type.to_s.capitalize
37
41
  count_lines << tui.text_line(spans: [
38
- tui.text_span(content: " #{sub_label}: ", style: state.dimmed_style),
39
- tui.text_span(content: sub_count.to_s, style: state.dimmed_style),
42
+ tui.text_span(content: " #{sub_label}: ", style: dimmed_style),
43
+ tui.text_span(content: sub_count.to_s, style: dimmed_style),
40
44
  ])
41
45
  end
42
46
  end
@@ -47,7 +51,7 @@ class View::Counts
47
51
  block: tui.block(
48
52
  title: "Event Counts",
49
53
  borders: [:all],
50
- border_style: tui.style(fg: state.border_color)
54
+ border_style: tui.style(fg: border_color)
51
55
  )
52
56
  )
53
57
  frame.render_widget(widget, area)
@@ -17,19 +17,20 @@ require_relative "../view"
17
17
  # === Examples
18
18
  #
19
19
  # live_view = View::Live.new
20
- # live_view.call(state, tui, frame, area)
20
+ # live_view.call(model, tui, frame, area)
21
21
  class View::Live
22
22
  # Renders the live event table to the given area.
23
23
  #
24
- # [state] ViewState containing event data and styles.
24
+ # [model] AppModel containing event data.
25
25
  # [tui] RatatuiRuby instance.
26
26
  # [frame] RatatuiRuby::Frame being rendered.
27
27
  # [area] RatatuiRuby::Rect defining the widget's bounds.
28
28
  #
29
29
  # === Example
30
30
  #
31
- # live_view.call(state, tui, frame, area)
32
- def call(state, tui, frame, area)
31
+ # live_view.call(model, tui, frame, area)
32
+ def call(model, tui, frame, area)
33
+ border_color = model.focused ? :green : :gray
33
34
  rows = []
34
35
 
35
36
  rows << tui.text_line(spans: [
@@ -39,13 +40,13 @@ class View::Live
39
40
  ])
40
41
 
41
42
  (AppAllEvents::EVENT_TYPES - [:none]).each do |type|
42
- event_data = state.events.live_event(type)
43
+ event_data = model.live_event(type)
43
44
 
44
45
  class_str = type.to_s.capitalize
45
46
  time_str = event_data ? event_data[:time].strftime("%H:%M:%S") : "—"
46
47
  desc_str = event_data ? event_data[:description] : "—"
47
48
 
48
- is_lit = state.events.lit?(type)
49
+ is_lit = model.lit?(type)
49
50
  row_style = is_lit ? tui.style(fg: :black, bg: :green) : nil
50
51
 
51
52
  rows << tui.text_line(spans: [
@@ -61,7 +62,7 @@ class View::Live
61
62
  block: tui.block(
62
63
  title: "Live Display",
63
64
  borders: [:all],
64
- border_style: tui.style(fg: state.border_color)
65
+ border_style: tui.style(fg: border_color)
65
66
  )
66
67
  )
67
68
  frame.render_widget(widget, area)
@@ -16,32 +16,27 @@ require_relative "../view"
16
16
  class View::Log
17
17
  # Renders the event log widget to the given area.
18
18
  #
19
- # [state] ViewState containing event data and styles.
19
+ # [model] AppModel containing event data.
20
20
  # [tui] RatatuiRuby instance.
21
21
  # [frame] RatatuiRuby::Frame being rendered.
22
22
  # [area] RatatuiRuby::Rect defining the widget's bounds.
23
- def call(state, tui, frame, area)
23
+ def call(model, tui, frame, area)
24
+ dimmed_style = tui.style(fg: :dark_gray)
25
+ border_color = model.focused ? :green : :gray
26
+
24
27
  visible_entries_count = (area.height - 2) / 2
25
- display_entries = state.events.visible(visible_entries_count)
28
+ display_entries = model.visible(visible_entries_count)
26
29
 
27
30
  log_lines = []
28
- if state.events.empty?
29
- log_lines << tui.text_line(spans: [tui.text_span(content: "No events yet...", style: state.dimmed_style)])
31
+ if model.empty?
32
+ log_lines << tui.text_line(spans: [tui.text_span(content: "No events yet...", style: dimmed_style)])
30
33
  else
31
34
  display_entries.each do |entry|
32
35
  entry_style = tui.style(fg: entry.color)
33
-
34
- # Split description into lines if it's too long, or just let it wrap conceptually (though paragraph wraps by character by default)
35
- # Using simple inspect output as requested.
36
36
  description = entry.description
37
37
 
38
- # We want to display it over potentially multiple lines if needed, but the original code did manual 2-line formatting.
39
- # Let's try to just dump the inspect string. If it's very long, it might be cut off.
40
- # But the User asked specifically to use inspect.
41
-
42
38
  log_lines << tui.text_line(spans: [tui.text_span(content: description, style: entry_style)])
43
- log_lines << tui.text_line(spans: [tui.text_span(content: "", style: entry_style)]) # Spacer line to match previous 2-line rhythm? Or just compact?
44
- # Previous view had 2 lines per entry. Let's keep a spacer to make it readable.
39
+ log_lines << tui.text_line(spans: [tui.text_span(content: "", style: entry_style)])
45
40
  end
46
41
  end
47
42
 
@@ -52,7 +47,7 @@ class View::Log
52
47
  block: tui.block(
53
48
  title: "Event Log",
54
49
  borders: [:all],
55
- border_style: tui.style(fg: state.border_color)
50
+ border_style: tui.style(fg: border_color)
56
51
  )
57
52
  )
58
53
  frame.render_widget(widget, area)