ratatui_ruby 0.9.1 → 0.10.1

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 (268) 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 +2 -1
  7. data/CHANGELOG.md +113 -0
  8. data/README.md +17 -0
  9. data/REUSE.toml +5 -0
  10. data/Rakefile +1 -1
  11. data/Steepfile +49 -0
  12. data/doc/concepts/debugging.md +401 -0
  13. data/doc/getting_started/quickstart.md +8 -3
  14. data/doc/images/app_all_events.png +0 -0
  15. data/doc/images/app_color_picker.png +0 -0
  16. data/doc/images/app_debugging_showcase.gif +0 -0
  17. data/doc/images/app_debugging_showcase.png +0 -0
  18. data/doc/images/app_login_form.png +0 -0
  19. data/doc/images/app_stateful_interaction.png +0 -0
  20. data/doc/images/verify_quickstart_dsl.png +0 -0
  21. data/doc/images/verify_quickstart_layout.png +0 -0
  22. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  23. data/doc/images/verify_readme_usage.png +0 -0
  24. data/doc/images/widget_barchart.png +0 -0
  25. data/doc/images/widget_block.png +0 -0
  26. data/doc/images/widget_box.png +0 -0
  27. data/doc/images/widget_calendar.png +0 -0
  28. data/doc/images/widget_canvas.png +0 -0
  29. data/doc/images/widget_cell.png +0 -0
  30. data/doc/images/widget_center.png +0 -0
  31. data/doc/images/widget_chart.png +0 -0
  32. data/doc/images/widget_gauge.png +0 -0
  33. data/doc/images/widget_layout_split.png +0 -0
  34. data/doc/images/widget_line_gauge.png +0 -0
  35. data/doc/images/widget_list.png +0 -0
  36. data/doc/images/widget_map.png +0 -0
  37. data/doc/images/widget_overlay.png +0 -0
  38. data/doc/images/widget_popup.png +0 -0
  39. data/doc/images/widget_ratatui_logo.png +0 -0
  40. data/doc/images/widget_ratatui_mascot.png +0 -0
  41. data/doc/images/widget_rect.png +0 -0
  42. data/doc/images/widget_render.png +0 -0
  43. data/doc/images/widget_rich_text.png +0 -0
  44. data/doc/images/widget_scroll_text.png +0 -0
  45. data/doc/images/widget_scrollbar.png +0 -0
  46. data/doc/images/widget_sparkline.png +0 -0
  47. data/doc/images/widget_style_colors.png +0 -0
  48. data/doc/images/widget_table.png +0 -0
  49. data/doc/images/widget_tabs.png +0 -0
  50. data/doc/images/widget_text_width.png +0 -0
  51. data/doc/troubleshooting/async.md +4 -0
  52. data/examples/app_debugging_showcase/README.md +119 -0
  53. data/examples/app_debugging_showcase/app.rb +318 -0
  54. data/examples/widget_canvas/app.rb +19 -14
  55. data/examples/widget_gauge/app.rb +18 -3
  56. data/examples/widget_layout_split/app.rb +10 -4
  57. data/examples/widget_list/app.rb +22 -6
  58. data/examples/widget_rect/app.rb +7 -6
  59. data/examples/widget_rich_text/app.rb +62 -37
  60. data/examples/widget_style_colors/app.rb +26 -47
  61. data/examples/widget_table/app.rb +28 -5
  62. data/examples/widget_text_width/app.rb +6 -4
  63. data/ext/ratatui_ruby/Cargo.lock +48 -1
  64. data/ext/ratatui_ruby/Cargo.toml +6 -2
  65. data/ext/ratatui_ruby/src/color.rs +82 -0
  66. data/ext/ratatui_ruby/src/errors.rs +28 -0
  67. data/ext/ratatui_ruby/src/events.rs +15 -14
  68. data/ext/ratatui_ruby/src/lib.rs +56 -0
  69. data/ext/ratatui_ruby/src/rendering.rs +3 -1
  70. data/ext/ratatui_ruby/src/style.rs +48 -21
  71. data/ext/ratatui_ruby/src/terminal.rs +40 -9
  72. data/ext/ratatui_ruby/src/text.rs +21 -9
  73. data/ext/ratatui_ruby/src/widgets/chart.rs +2 -1
  74. data/ext/ratatui_ruby/src/widgets/layout.rs +90 -2
  75. data/ext/ratatui_ruby/src/widgets/list.rs +6 -5
  76. data/ext/ratatui_ruby/src/widgets/overlay.rs +2 -1
  77. data/ext/ratatui_ruby/src/widgets/table.rs +7 -6
  78. data/ext/ratatui_ruby/src/widgets/table_state.rs +55 -0
  79. data/ext/ratatui_ruby/src/widgets/tabs.rs +3 -2
  80. data/lib/ratatui_ruby/buffer/cell.rb +25 -15
  81. data/lib/ratatui_ruby/buffer.rb +134 -2
  82. data/lib/ratatui_ruby/cell.rb +13 -5
  83. data/lib/ratatui_ruby/debug.rb +215 -0
  84. data/lib/ratatui_ruby/event/key.rb +3 -2
  85. data/lib/ratatui_ruby/event.rb +1 -1
  86. data/lib/ratatui_ruby/layout/constraint.rb +49 -0
  87. data/lib/ratatui_ruby/layout/layout.rb +119 -13
  88. data/lib/ratatui_ruby/layout/position.rb +55 -0
  89. data/lib/ratatui_ruby/layout/rect.rb +188 -0
  90. data/lib/ratatui_ruby/layout/size.rb +55 -0
  91. data/lib/ratatui_ruby/layout.rb +4 -0
  92. data/lib/ratatui_ruby/style/color.rb +149 -0
  93. data/lib/ratatui_ruby/style/style.rb +51 -4
  94. data/lib/ratatui_ruby/style.rb +2 -0
  95. data/lib/ratatui_ruby/symbols.rb +435 -0
  96. data/lib/ratatui_ruby/synthetic_events.rb +1 -1
  97. data/lib/ratatui_ruby/table_state.rb +51 -0
  98. data/lib/ratatui_ruby/terminal_lifecycle.rb +2 -1
  99. data/lib/ratatui_ruby/test_helper/event_injection.rb +6 -1
  100. data/lib/ratatui_ruby/test_helper.rb +9 -0
  101. data/lib/ratatui_ruby/text/line.rb +245 -0
  102. data/lib/ratatui_ruby/text/span.rb +158 -0
  103. data/lib/ratatui_ruby/text.rb +99 -0
  104. data/lib/ratatui_ruby/tui/canvas_factories.rb +103 -0
  105. data/lib/ratatui_ruby/tui/core.rb +13 -2
  106. data/lib/ratatui_ruby/tui/layout_factories.rb +50 -3
  107. data/lib/ratatui_ruby/tui/state_factories.rb +42 -0
  108. data/lib/ratatui_ruby/tui/text_factories.rb +40 -0
  109. data/lib/ratatui_ruby/tui/widget_factories.rb +135 -60
  110. data/lib/ratatui_ruby/tui.rb +22 -1
  111. data/lib/ratatui_ruby/version.rb +1 -1
  112. data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +2 -0
  113. data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +2 -0
  114. data/lib/ratatui_ruby/widgets/bar_chart.rb +30 -20
  115. data/lib/ratatui_ruby/widgets/block.rb +14 -6
  116. data/lib/ratatui_ruby/widgets/calendar.rb +2 -0
  117. data/lib/ratatui_ruby/widgets/canvas.rb +56 -0
  118. data/lib/ratatui_ruby/widgets/cell.rb +2 -0
  119. data/lib/ratatui_ruby/widgets/center.rb +2 -0
  120. data/lib/ratatui_ruby/widgets/chart.rb +6 -0
  121. data/lib/ratatui_ruby/widgets/clear.rb +2 -0
  122. data/lib/ratatui_ruby/widgets/coerceable_widget.rb +77 -0
  123. data/lib/ratatui_ruby/widgets/cursor.rb +2 -0
  124. data/lib/ratatui_ruby/widgets/gauge.rb +61 -3
  125. data/lib/ratatui_ruby/widgets/line_gauge.rb +66 -4
  126. data/lib/ratatui_ruby/widgets/list.rb +87 -3
  127. data/lib/ratatui_ruby/widgets/list_item.rb +2 -0
  128. data/lib/ratatui_ruby/widgets/overlay.rb +2 -0
  129. data/lib/ratatui_ruby/widgets/paragraph.rb +4 -0
  130. data/lib/ratatui_ruby/widgets/ratatui_logo.rb +2 -0
  131. data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +2 -0
  132. data/lib/ratatui_ruby/widgets/row.rb +45 -0
  133. data/lib/ratatui_ruby/widgets/scrollbar.rb +2 -0
  134. data/lib/ratatui_ruby/widgets/shape/label.rb +2 -0
  135. data/lib/ratatui_ruby/widgets/sparkline.rb +21 -13
  136. data/lib/ratatui_ruby/widgets/table.rb +13 -3
  137. data/lib/ratatui_ruby/widgets/tabs.rb +6 -4
  138. data/lib/ratatui_ruby/widgets.rb +1 -0
  139. data/lib/ratatui_ruby.rb +42 -11
  140. data/sig/examples/app_all_events/model/app_model.rbs +23 -0
  141. data/sig/examples/app_all_events/model/event_entry.rbs +15 -8
  142. data/sig/examples/app_all_events/model/timestamp.rbs +1 -1
  143. data/sig/examples/app_all_events/view.rbs +1 -1
  144. data/sig/examples/app_stateful_interaction/app.rbs +5 -5
  145. data/sig/examples/widget_block_demo/app.rbs +6 -6
  146. data/sig/manifest.yaml +5 -0
  147. data/sig/patches/data.rbs +26 -0
  148. data/sig/patches/debugger__.rbs +8 -0
  149. data/sig/ratatui_ruby/buffer/cell.rbs +46 -0
  150. data/sig/ratatui_ruby/buffer.rbs +18 -0
  151. data/sig/ratatui_ruby/cell.rbs +44 -0
  152. data/sig/ratatui_ruby/clear.rbs +18 -0
  153. data/sig/ratatui_ruby/constraint.rbs +26 -0
  154. data/sig/ratatui_ruby/debug.rbs +45 -0
  155. data/sig/ratatui_ruby/draw.rbs +30 -0
  156. data/sig/ratatui_ruby/event.rbs +68 -8
  157. data/sig/ratatui_ruby/frame.rbs +4 -4
  158. data/sig/ratatui_ruby/interfaces.rbs +25 -0
  159. data/sig/ratatui_ruby/layout/constraint.rbs +39 -0
  160. data/sig/ratatui_ruby/layout/layout.rbs +45 -0
  161. data/sig/ratatui_ruby/layout/position.rbs +18 -0
  162. data/sig/ratatui_ruby/layout/rect.rbs +64 -0
  163. data/sig/ratatui_ruby/layout/size.rbs +18 -0
  164. data/sig/ratatui_ruby/output_guard.rbs +23 -0
  165. data/sig/ratatui_ruby/ratatui_ruby.rbs +84 -5
  166. data/sig/ratatui_ruby/rect.rbs +17 -0
  167. data/sig/ratatui_ruby/style/color.rbs +22 -0
  168. data/sig/ratatui_ruby/style/style.rbs +29 -0
  169. data/sig/ratatui_ruby/symbols.rbs +141 -0
  170. data/sig/ratatui_ruby/synthetic_events.rbs +21 -0
  171. data/sig/ratatui_ruby/table_state.rbs +6 -0
  172. data/sig/ratatui_ruby/terminal_lifecycle.rbs +31 -0
  173. data/sig/ratatui_ruby/test_helper/event_injection.rbs +2 -2
  174. data/sig/ratatui_ruby/test_helper/snapshot.rbs +22 -3
  175. data/sig/ratatui_ruby/test_helper/style_assertions.rbs +8 -1
  176. data/sig/ratatui_ruby/test_helper/test_doubles.rbs +7 -3
  177. data/sig/ratatui_ruby/text/line.rbs +27 -0
  178. data/sig/ratatui_ruby/text/span.rbs +23 -0
  179. data/sig/ratatui_ruby/text.rbs +12 -0
  180. data/sig/ratatui_ruby/tui/buffer_factories.rbs +1 -1
  181. data/sig/ratatui_ruby/tui/canvas_factories.rbs +23 -5
  182. data/sig/ratatui_ruby/tui/core.rbs +2 -2
  183. data/sig/ratatui_ruby/tui/layout_factories.rbs +16 -2
  184. data/sig/ratatui_ruby/tui/state_factories.rbs +8 -3
  185. data/sig/ratatui_ruby/tui/style_factories.rbs +3 -1
  186. data/sig/ratatui_ruby/tui/text_factories.rbs +7 -4
  187. data/sig/ratatui_ruby/tui/widget_factories.rbs +123 -30
  188. data/sig/ratatui_ruby/widgets/bar_chart.rbs +95 -0
  189. data/sig/ratatui_ruby/widgets/block.rbs +51 -0
  190. data/sig/ratatui_ruby/widgets/calendar.rbs +45 -0
  191. data/sig/ratatui_ruby/widgets/canvas.rbs +95 -0
  192. data/sig/ratatui_ruby/widgets/chart.rbs +91 -0
  193. data/sig/ratatui_ruby/widgets/coerceable_widget.rbs +26 -0
  194. data/sig/ratatui_ruby/widgets/gauge.rbs +44 -0
  195. data/sig/ratatui_ruby/widgets/line_gauge.rbs +48 -0
  196. data/sig/ratatui_ruby/widgets/list.rbs +63 -0
  197. data/sig/ratatui_ruby/widgets/misc.rbs +158 -0
  198. data/sig/ratatui_ruby/widgets/paragraph.rbs +45 -0
  199. data/sig/ratatui_ruby/widgets/row.rbs +43 -0
  200. data/sig/ratatui_ruby/widgets/scrollbar.rbs +53 -0
  201. data/sig/ratatui_ruby/widgets/shape/label.rbs +37 -0
  202. data/sig/ratatui_ruby/widgets/sparkline.rbs +45 -0
  203. data/sig/ratatui_ruby/widgets/table.rbs +78 -0
  204. data/sig/ratatui_ruby/widgets/tabs.rbs +44 -0
  205. data/sig/ratatui_ruby/{schema/list_item.rbs → widgets.rbs} +4 -4
  206. data/tasks/steep.rake +11 -0
  207. metadata +80 -63
  208. data/doc/contributors/v1.0.0_blockers.md +0 -870
  209. data/doc/troubleshooting/debugging.md +0 -101
  210. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +0 -47
  211. data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +0 -25
  212. data/lib/ratatui_ruby/schema/bar_chart.rb +0 -287
  213. data/lib/ratatui_ruby/schema/block.rb +0 -198
  214. data/lib/ratatui_ruby/schema/calendar.rb +0 -84
  215. data/lib/ratatui_ruby/schema/canvas.rb +0 -239
  216. data/lib/ratatui_ruby/schema/center.rb +0 -67
  217. data/lib/ratatui_ruby/schema/chart.rb +0 -159
  218. data/lib/ratatui_ruby/schema/clear.rb +0 -62
  219. data/lib/ratatui_ruby/schema/constraint.rb +0 -151
  220. data/lib/ratatui_ruby/schema/cursor.rb +0 -50
  221. data/lib/ratatui_ruby/schema/gauge.rb +0 -72
  222. data/lib/ratatui_ruby/schema/layout.rb +0 -122
  223. data/lib/ratatui_ruby/schema/line_gauge.rb +0 -80
  224. data/lib/ratatui_ruby/schema/list.rb +0 -135
  225. data/lib/ratatui_ruby/schema/list_item.rb +0 -51
  226. data/lib/ratatui_ruby/schema/overlay.rb +0 -51
  227. data/lib/ratatui_ruby/schema/paragraph.rb +0 -107
  228. data/lib/ratatui_ruby/schema/ratatui_logo.rb +0 -31
  229. data/lib/ratatui_ruby/schema/ratatui_mascot.rb +0 -36
  230. data/lib/ratatui_ruby/schema/rect.rb +0 -174
  231. data/lib/ratatui_ruby/schema/row.rb +0 -76
  232. data/lib/ratatui_ruby/schema/scrollbar.rb +0 -143
  233. data/lib/ratatui_ruby/schema/shape/label.rb +0 -76
  234. data/lib/ratatui_ruby/schema/sparkline.rb +0 -142
  235. data/lib/ratatui_ruby/schema/style.rb +0 -97
  236. data/lib/ratatui_ruby/schema/table.rb +0 -141
  237. data/lib/ratatui_ruby/schema/tabs.rb +0 -85
  238. data/lib/ratatui_ruby/schema/text.rb +0 -217
  239. data/sig/examples/app_all_events/model/events.rbs +0 -15
  240. data/sig/examples/app_all_events/view_state.rbs +0 -21
  241. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +0 -22
  242. data/sig/ratatui_ruby/schema/bar_chart/bar_group.rbs +0 -19
  243. data/sig/ratatui_ruby/schema/bar_chart.rbs +0 -38
  244. data/sig/ratatui_ruby/schema/block.rbs +0 -18
  245. data/sig/ratatui_ruby/schema/calendar.rbs +0 -23
  246. data/sig/ratatui_ruby/schema/canvas.rbs +0 -81
  247. data/sig/ratatui_ruby/schema/center.rbs +0 -17
  248. data/sig/ratatui_ruby/schema/chart.rbs +0 -39
  249. data/sig/ratatui_ruby/schema/constraint.rbs +0 -30
  250. data/sig/ratatui_ruby/schema/cursor.rbs +0 -16
  251. data/sig/ratatui_ruby/schema/draw.rbs +0 -33
  252. data/sig/ratatui_ruby/schema/gauge.rbs +0 -23
  253. data/sig/ratatui_ruby/schema/layout.rbs +0 -27
  254. data/sig/ratatui_ruby/schema/line_gauge.rbs +0 -24
  255. data/sig/ratatui_ruby/schema/list.rbs +0 -28
  256. data/sig/ratatui_ruby/schema/overlay.rbs +0 -15
  257. data/sig/ratatui_ruby/schema/paragraph.rbs +0 -20
  258. data/sig/ratatui_ruby/schema/ratatui_logo.rbs +0 -14
  259. data/sig/ratatui_ruby/schema/ratatui_mascot.rbs +0 -17
  260. data/sig/ratatui_ruby/schema/rect.rbs +0 -48
  261. data/sig/ratatui_ruby/schema/row.rbs +0 -28
  262. data/sig/ratatui_ruby/schema/scrollbar.rbs +0 -42
  263. data/sig/ratatui_ruby/schema/sparkline.rbs +0 -22
  264. data/sig/ratatui_ruby/schema/style.rbs +0 -19
  265. data/sig/ratatui_ruby/schema/table.rbs +0 -32
  266. data/sig/ratatui_ruby/schema/tabs.rbs +0 -21
  267. data/sig/ratatui_ruby/schema/text.rbs +0 -31
  268. /data/lib/ratatui_ruby/{schema/draw.rb → draw.rb} +0 -0
@@ -0,0 +1,318 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: MIT-0
6
+ #++
7
+
8
+ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
9
+
10
+ require "ratatui_ruby"
11
+
12
+ ##
13
+ # Interactive demonstration of RatatuiRuby debugging features.
14
+ #
15
+ # This example lets you trigger each debugging feature with a hotkey to verify
16
+ # your setup works before encountering a real bug.
17
+ #
18
+ # == Hotkeys
19
+ #
20
+ # [d] Enable debug_mode! — Shows the debug socket path for remote attachment
21
+ # [p] Trigger test_panic! — Deliberately crashes to verify Rust backtrace visibility
22
+ # [t] Cause TypeError — Passes wrong type to widget factory to show Rust stack frames
23
+ # [b] Show backtrace status — Displays current debug configuration
24
+ # [q] Quit
25
+ #
26
+ # == Usage
27
+ #
28
+ # # Normal mode (no backtraces):
29
+ # ruby examples/verify_debugging_usage/app.rb
30
+ #
31
+ # # With Rust backtraces only:
32
+ # RUST_BACKTRACE=1 ruby examples/verify_debugging_usage/app.rb
33
+ #
34
+ # # Full debug mode (stops at startup for debugger attachment):
35
+ # RR_DEBUG=1 ruby examples/verify_debugging_usage/app.rb
36
+ #
37
+ # == Remote Debugging
38
+ #
39
+ # When you press [d] to enable debug_mode!, the app continues running but
40
+ # prints a socket path. From another terminal:
41
+ #
42
+ # rdbg --attach
43
+ #
44
+ # This gives you a full debugger REPL while the TUI keeps running.
45
+ class VerifyDebuggingUsage
46
+ def initialize
47
+ @status_message = "Press a key to test debugging features"
48
+ @show_debug_info = false
49
+ @quit = false
50
+
51
+ # If debug mode was enabled via RR_DEBUG=1 at startup, capture the socket path
52
+ if RatatuiRuby::Debug.enabled?
53
+ @socket_path = begin
54
+ ::DEBUGGER__.create_unix_domain_socket_name
55
+ rescue NameError
56
+ nil
57
+ end
58
+ @show_debug_info = true
59
+ @status_message = "RR_DEBUG=1 detected — debug mode active"
60
+ end
61
+ end
62
+
63
+ def run
64
+ RatatuiRuby.run do |tui|
65
+ @tui = tui
66
+ @loop_count = 0
67
+
68
+ loop do
69
+ @loop_count += 1
70
+
71
+ # 🎯 Breakpoint every 250 loops. Try: p @status_message
72
+ if RatatuiRuby::Debug.enabled? && (@loop_count % 250).zero?
73
+ you_found_me = "🎉 You found me! Loop ##{@loop_count}"
74
+ # rubocop:disable Lint/Debugger
75
+ debugger
76
+ # rubocop:enable Lint/Debugger
77
+ _ = you_found_me # Suppress unused variable warning
78
+ end
79
+
80
+ render
81
+ break if @quit || handle_input == :quit
82
+ end
83
+ end
84
+ end
85
+
86
+ private def render
87
+ @tui.draw do |frame|
88
+ constraints = [
89
+ @tui.constraint_length(3), # Status
90
+ @tui.constraint_length(5), # Config
91
+ @tui.constraint_length(6), # Actions
92
+ ]
93
+
94
+ if @show_debug_info
95
+ constraints << @tui.constraint_length(6) # Debug info
96
+ end
97
+
98
+ constraints << @tui.constraint_fill(1) # Spacer
99
+ constraints << @tui.constraint_length(3) # Help
100
+
101
+ chunks = @tui.layout_split(frame.area, direction: :vertical, constraints:)
102
+
103
+ idx = 0
104
+ render_status(frame, chunks[idx])
105
+ idx += 1
106
+ render_config(frame, chunks[idx])
107
+ idx += 1
108
+ render_actions(frame, chunks[idx])
109
+ idx += 1
110
+
111
+ if @show_debug_info
112
+ render_debug_info(frame, chunks[idx])
113
+ idx += 1
114
+ end
115
+
116
+ # Skip spacer
117
+ idx += 1
118
+ render_help(frame, chunks[idx])
119
+ end
120
+ end
121
+
122
+ private def render_status(frame, area)
123
+ frame.render_widget(
124
+ @tui.paragraph(
125
+ text: @status_message,
126
+ alignment: :center,
127
+ block: @tui.block(
128
+ title: " Status ",
129
+ title_alignment: :center,
130
+ borders: [:all],
131
+ border_style: { fg: :yellow }
132
+ )
133
+ ),
134
+ area
135
+ )
136
+ end
137
+
138
+ private def render_config(frame, area)
139
+ config_lines = [
140
+ "Rust Backtraces: #{flag(RatatuiRuby::Debug.rust_backtrace_enabled?)}",
141
+ "Full Debug Mode: #{flag(RatatuiRuby::Debug.enabled?)}",
142
+ "Remote Debugging: #{remote_mode_description}",
143
+ ].join("\n")
144
+
145
+ frame.render_widget(
146
+ @tui.paragraph(
147
+ text: config_lines,
148
+ block: @tui.block(
149
+ title: " Current Debug Configuration ",
150
+ borders: [:all],
151
+ border_style: { fg: :cyan }
152
+ )
153
+ ),
154
+ area
155
+ )
156
+ end
157
+
158
+ private def render_actions(frame, area)
159
+ actions_lines = [
160
+ "[d] Enable debug_mode! and show socket info",
161
+ "[p] Trigger test_panic! to verify backtrace visibility",
162
+ "[t] Cause TypeError (pass wrong type to widget)",
163
+ "[b] Refresh debug status",
164
+ ].join("\n")
165
+
166
+ frame.render_widget(
167
+ @tui.paragraph(
168
+ text: actions_lines,
169
+ block: @tui.block(
170
+ title: " Available Actions ",
171
+ borders: [:all],
172
+ border_style: { fg: :green }
173
+ )
174
+ ),
175
+ area
176
+ )
177
+ end
178
+
179
+ private def render_debug_info(frame, area)
180
+ socket_display = @socket_path || "(socket not available)"
181
+ info_lines = [
182
+ "Socket: #{socket_display}",
183
+ "Attach: rdbg --attach",
184
+ "Hint: type 'continue' if you see SIGURG",
185
+ ]
186
+
187
+ frame.render_widget(
188
+ @tui.paragraph(
189
+ text: info_lines.join("\n"),
190
+ block: @tui.block(
191
+ title: " Remote Debugging ",
192
+ borders: [:all],
193
+ border_style: { fg: :magenta }
194
+ )
195
+ ),
196
+ area
197
+ )
198
+ end
199
+
200
+ private def render_help(frame, area)
201
+ frame.render_widget(
202
+ @tui.paragraph(
203
+ text: "[d] debug_mode! [p] test_panic! [t] TypeError [b] status [q] quit",
204
+ alignment: :center,
205
+ block: @tui.block(
206
+ borders: [:all],
207
+ border_style: { fg: :dark_gray }
208
+ )
209
+ ),
210
+ area
211
+ )
212
+ end
213
+
214
+ private def flag(value)
215
+ value ? "✓ enabled" : "✗ disabled"
216
+ end
217
+
218
+ private def remote_mode_description
219
+ case RatatuiRuby::Debug.remote_debugging_mode
220
+ when :open
221
+ attached = debugger_attached? ? " — ATTACHED" : " — waiting"
222
+ "✓ open#{attached}"
223
+ when :open_nonstop
224
+ attached = debugger_attached? ? " — ATTACHED" : ""
225
+ "✓ open_nonstop#{attached}"
226
+ else
227
+ "✗ not configured"
228
+ end
229
+ end
230
+
231
+ # ☣️ FRAGILE: This pokes at debug gem internals.
232
+ #
233
+ # Private instance variables can change between gem versions. This code
234
+ # may silently break. We accept that risk here because this showcase
235
+ # exists specifically to demonstrate debugger attachment status.
236
+ #
237
+ # For production apps, checking Debug.enabled? is sufficient — knowing
238
+ # whether a client has attached rarely matters.
239
+ private def debugger_attached?
240
+ return false unless defined?(::DEBUGGER__::SESSION)
241
+
242
+ ui = ::DEBUGGER__::SESSION.instance_variable_get(:@ui)
243
+ return false unless ui
244
+
245
+ # The @sock instance variable is set when a client connects
246
+ sock = ui.instance_variable_get(:@sock)
247
+ !sock.nil?
248
+ rescue
249
+ false
250
+ end
251
+
252
+ private def handle_input
253
+ case @tui.poll_event
254
+ in { type: :key, code: "q" } | { type: :key, code: "c", modifiers: ["ctrl"] }
255
+ :quit
256
+
257
+ in { type: :key, code: "d" }
258
+ enable_debug_mode!
259
+
260
+ in { type: :key, code: "p" }
261
+ trigger_test_panic!
262
+
263
+ in { type: :key, code: "t" }
264
+ trigger_type_error!
265
+
266
+ in { type: :key, code: "b" }
267
+ @status_message = "Debug status refreshed at #{Time.now.strftime('%H:%M:%S')}"
268
+
269
+ else
270
+ nil
271
+ end
272
+ end
273
+
274
+ private def enable_debug_mode!
275
+ if RatatuiRuby::Debug.enabled?
276
+ @status_message = "Debug mode already enabled!"
277
+ else
278
+ # debug_mode! returns the socket path and suppresses the debug gem's output
279
+ @socket_path = RatatuiRuby.debug_mode!
280
+ @status_message = "debug_mode! enabled"
281
+ @show_debug_info = true
282
+ end
283
+ end
284
+
285
+ private def trigger_test_panic!
286
+ if RatatuiRuby::Debug.rust_backtrace_enabled?
287
+ @status_message = "Triggering test_panic! — check stderr for backtrace..."
288
+ else
289
+ @status_message = "Triggering test_panic! — backtrace hidden (set RUST_BACKTRACE=1)"
290
+ end
291
+ render # Show the message before crashing
292
+
293
+ # Give a moment for the render to complete
294
+ sleep 0.1
295
+
296
+ # This will crash the app with a Rust panic. If RUST_BACKTRACE=1 or
297
+ # debug mode is enabled, you'll see the full Rust stack trace after
298
+ # the terminal is restored.
299
+ RatatuiRuby::Debug.test_panic!
300
+ end
301
+
302
+ private def trigger_type_error!
303
+ if RatatuiRuby::Debug.rust_backtrace_enabled?
304
+ @status_message = "Triggering TypeError — check stderr for error message..."
305
+ else
306
+ @status_message = "Triggering TypeError — set RUST_BACKTRACE=1 for stack trace"
307
+ end
308
+ render # Show the message before crashing
309
+ sleep 0.1
310
+
311
+ # Bypass the factory's DWIM coercion to trigger a real Rust TypeError.
312
+ # Uses Widgets::Table.new directly with invalid rows type.
313
+ bad_table = RatatuiRuby::Widgets::Table.new(rows: 42, widths: [])
314
+ @tui.draw { |f| f.render_widget(bad_table, f.area) }
315
+ end
316
+ end
317
+
318
+ VerifyDebuggingUsage.new.run if __FILE__ == $PROGRAM_NAME
@@ -37,24 +37,26 @@ class WidgetCanvas
37
37
 
38
38
  private def render
39
39
  @tui.draw do |frame|
40
- # Define shapes
40
+ # Define shapes using terse aliases (circle, rectangle, point, map, label)
41
+ # These are shorter forms of shape_circle, shape_rectangle, etc.
41
42
  shapes = []
42
43
 
43
- # 1. Static Grid (Lines)
44
+ # 1. Static Grid (Lines) - using shape_line (no terse alias for line)
44
45
  (-100..100).step(20) do |i|
45
46
  shapes << @tui.shape_line(x1: i.to_f, y1: -100.0, x2: i.to_f, y2: 100.0, color: :gray)
46
47
  shapes << @tui.shape_line(x1: -100.0, y1: i.to_f, x2: 100.0, y2: i.to_f, color: :gray)
47
48
  end
48
49
 
49
- # 2. Moving Circle (The "Player")
50
- shapes << @tui.shape_circle(
50
+ # 2. Moving Circle (The "Player") - using terse 'circle' alias
51
+ shapes << @tui.circle(
51
52
  x: @x_offset,
52
53
  y: @y_offset,
53
54
  radius: 10.0,
54
55
  color: :green
55
56
  )
56
57
 
57
- # 3. Static Rectangle (Target)
58
+ # 3. Static Rectangle (Target) - using shape_rectangle (no 'rectangle' alias
59
+ # to avoid confusion with Layout::Rect)
58
60
  shapes << @tui.shape_rectangle(
59
61
  x: 30.0,
60
62
  y: 30.0,
@@ -63,16 +65,16 @@ class WidgetCanvas
63
65
  color: :red
64
66
  )
65
67
 
66
- # 4. Points (Starfield)
68
+ # 4. Points (Starfield) - using terse 'point' alias
67
69
  # Deterministic "random" points
68
70
  10.times do |i|
69
- shapes << @tui.shape_point(
71
+ shapes << @tui.point(
70
72
  x: ((i * 37) % 200) - 100.0,
71
73
  y: ((i * 19) % 200) - 100.0
72
74
  )
73
75
  end
74
76
 
75
- # 5. Label
77
+ # 5. Connecting line from origin to player position
76
78
  shapes << @tui.shape_line(x1: 0.0, y1: 0.0, x2: @x_offset, y2: @y_offset, color: :yellow)
77
79
 
78
80
  canvas = @tui.canvas(
@@ -89,21 +91,24 @@ class WidgetCanvas
89
91
  direction: :vertical,
90
92
  constraints: [
91
93
  @tui.constraint_fill(1),
92
- @tui.constraint_length(3),
94
+ @tui.constraint_length(2),
93
95
  ]
94
96
  )
95
97
 
96
98
  frame.render_widget(canvas, layout[0])
97
99
 
98
- # Controls
100
+ # Query: Canvas#get_point maps canvas coordinates to normalized [0.0, 1.0] grid
101
+ normalized = canvas.get_point(@x_offset, @y_offset)
102
+ norm_str = normalized ? format("[%.2f, %.2f]", normalized[0], normalized[1]) : "nil"
103
+
104
+ # Controls showing query method demonstration (single concise line)
99
105
  controls = @tui.paragraph(
100
106
  text: [
101
- @tui.text_line(spans: [
102
- @tui.text_span(content: "Canvas auto-animates.", style: @tui.style(fg: :yellow)),
103
- ]),
104
107
  @tui.text_line(spans: [
105
108
  @tui.text_span(content: "q", style: @tui.style(modifiers: [:bold, :underlined])),
106
- @tui.text_span(content: ": Quit"),
109
+ @tui.text_span(content: ": Quit "),
110
+ @tui.text_span(content: "get_point → ", style: @tui.style(fg: :dark_gray)),
111
+ @tui.text_span(content: norm_str, style: @tui.style(fg: :cyan)),
107
112
  ]),
108
113
  ],
109
114
  block: @tui.block(borders: [:top])
@@ -29,6 +29,10 @@ class WidgetGauge
29
29
  @ratios = [0.0, 0.25, 0.5, 0.65, 0.8, 0.95, 1.0]
30
30
  @ratio_index = 3
31
31
 
32
+ # Demonstrates both ratio (0.0-1.0) and percent (0-100) input modes
33
+ @input_modes = [:ratio, :percent]
34
+ @input_mode_index = 0
35
+
32
36
  @gauge_colors = [
33
37
  { name: "Green", color: :green },
34
38
  { name: "Yellow", color: :yellow },
@@ -116,13 +120,20 @@ class WidgetGauge
116
120
  frame.render_widget(title, title_area)
117
121
 
118
122
  # Gauge 1: Main interactive gauge
123
+ # Demonstrates both ratio (0.0-1.0) and percent (0-100) input modes
124
+ input_mode = @input_modes[@input_mode_index]
125
+ gauge_opts = if input_mode == :percent
126
+ { percent: (@ratio * 100).to_i } # percent: accepts 0-100
127
+ else
128
+ { ratio: @ratio } # ratio: accepts 0.0-1.0
129
+ end
119
130
  gauge1 = @tui.gauge(
120
- ratio: @ratio,
131
+ **gauge_opts,
121
132
  label:,
122
133
  style: bg_style,
123
134
  gauge_style:,
124
135
  use_unicode:,
125
- block: @tui.block(title: "Interactive Gauge")
136
+ block: @tui.block(title: "Interactive Gauge (#{input_mode}:)")
126
137
  )
127
138
  frame.render_widget(gauge1, gauge1_area)
128
139
 
@@ -178,7 +189,9 @@ class WidgetGauge
178
189
  @tui.text_span(content: "u", style: @hotkey_style),
179
190
  @tui.text_span(content: ": Unicode (#{use_unicode ? 'On' : 'Off'}) "),
180
191
  @tui.text_span(content: "l", style: @hotkey_style),
181
- @tui.text_span(content: ": Label (#{@label_modes[@label_mode_index][:name]})"),
192
+ @tui.text_span(content: ": Label (#{@label_modes[@label_mode_index][:name]}) "),
193
+ @tui.text_span(content: "i", style: @hotkey_style),
194
+ @tui.text_span(content: ": Input (#{@input_modes[@input_mode_index]}:)"),
182
195
  ]),
183
196
  ]
184
197
  ),
@@ -204,6 +217,8 @@ class WidgetGauge
204
217
  @use_unicode_index = (@use_unicode_index + 1) % @use_unicode_options.length
205
218
  in type: :key, code: "l"
206
219
  @label_mode_index = (@label_mode_index + 1) % @label_modes.length
220
+ in type: :key, code: "i"
221
+ @input_mode_index = (@input_mode_index + 1) % @input_modes.length
207
222
  else
208
223
  # Ignore other events
209
224
  nil
@@ -206,6 +206,11 @@ class WidgetLayoutSplit
206
206
  end
207
207
 
208
208
  private def render_controls(frame, area)
209
+ # Demonstrate Constraint#call - show computed sizes for 100 units
210
+ constraints = current_constraints
211
+ applied = constraints.map { |c| c.(100) } # proc-like invocation
212
+ apply_str = "constraint.(100): #{applied.join(', ')}"
213
+
209
214
  controls = @tui.block(
210
215
  title: "Controls",
211
216
  borders: [:all],
@@ -221,13 +226,14 @@ class WidgetLayoutSplit
221
226
  ]),
222
227
  @tui.text_line(spans: [
223
228
  @tui.text_span(content: "c", style: @hotkey_style),
224
- @tui.text_span(content: ": Constraints (#{current_constraint_name})"),
225
- ]),
226
- # Row 3: Quit
227
- @tui.text_line(spans: [
229
+ @tui.text_span(content: ": Constraints (#{current_constraint_name}) "),
228
230
  @tui.text_span(content: "q", style: @hotkey_style),
229
231
  @tui.text_span(content: ": Quit"),
230
232
  ]),
233
+ # Row 3: Apply demonstration
234
+ @tui.text_line(spans: [
235
+ @tui.text_span(content: apply_str, style: @tui.style(fg: :dark_gray)),
236
+ ]),
231
237
  ]
232
238
  ),
233
239
  ]
@@ -230,8 +230,6 @@ class WidgetList
230
230
  # Determine selection/offset based on mode
231
231
  effective_selection = offset_mode_config[:allow_selection] ? @selected_index : nil
232
232
  effective_offset = offset_mode_config[:offset]
233
- selection_label = effective_selection.nil? ? "none" : effective_selection.to_s
234
- offset_label = effective_offset.nil? ? "auto" : effective_offset.to_s
235
233
 
236
234
  @tui.draw do |frame|
237
235
  # Split into main content and control panel
@@ -258,8 +256,11 @@ class WidgetList
258
256
  title = @tui.paragraph(text: "List Widget - Interactive Attribute Cycling")
259
257
  frame.render_widget(title, title_area)
260
258
 
261
- # Render list
262
- list = @tui.list(
259
+ # Build list first to demonstrate query methods:
260
+ # - List#len (with #length, #size aliases)
261
+ # - List#selection (alias for #selected_index)
262
+ # - List#selected_item (returns item at selection, or nil)
263
+ base_list = @tui.list(
263
264
  items:,
264
265
  selected_index: effective_selection,
265
266
  offset: effective_offset,
@@ -269,9 +270,24 @@ class WidgetList
269
270
  repeat_highlight_symbol: repeat_config[:repeat],
270
271
  highlight_spacing: spacing_config[:spacing],
271
272
  direction: direction_config[:direction],
272
- scroll_padding: scroll_padding_config[:padding],
273
+ scroll_padding: scroll_padding_config[:padding]
274
+ )
275
+
276
+ # Demonstrate query methods: len, selected_index, selected_item
277
+ item_count = base_list.len
278
+ current_index = base_list.selected_index # Explicit name - returns index
279
+ current_item = base_list.selected_item # Explicit name - returns item
280
+
281
+ # Format the selected item for display (handle rich text objects)
282
+ item_preview = case current_item
283
+ when nil then "none"
284
+ when String then (current_item.length > 12) ? "#{current_item[0..11]}…" : current_item
285
+ else current_item.class.name.split("::").last # Show type for rich text
286
+ end
287
+
288
+ list = base_list.with(
273
289
  block: @tui.block(
274
- title: "#{@item_sets[@item_set_index][:name]} | Sel: #{selection_label} | Offset: #{offset_label}",
290
+ title: "#{@item_sets[@item_set_index][:name]} (len: #{item_count}) | index: #{current_index.inspect} → #{item_preview}",
275
291
  borders: [:all]
276
292
  )
277
293
  )
@@ -97,6 +97,10 @@ class WidgetRect
97
97
  clamped = r.clamp(bounds)
98
98
  union_r = r.union(@sidebar_rect)
99
99
 
100
+ # Extract position and size from rect
101
+ pos = r.position # => Position(x:, y:)
102
+ size = r.size # => Size(width:, height:)
103
+
100
104
  text_content = [
101
105
  @tui.text_line(spans: [
102
106
  @tui.text_span(content: "Active View: ", style: @label_style),
@@ -112,19 +116,16 @@ class WidgetRect
112
116
  ]),
113
117
  " left:#{r.left} right:#{r.right} top:#{r.top} bottom:#{r.bottom}",
114
118
  @tui.text_line(spans: [
115
- @tui.text_span(content: "Size Methods:", style: @label_style),
119
+ @tui.text_span(content: "Conversion Methods ", style: @label_style),
120
+ @tui.text_span(content: "(as_position/as_size):", style: @dim_style),
116
121
  ]),
117
- " area:#{r.area} empty?:#{r.empty?}",
122
+ " Position: x=#{pos.x} y=#{pos.y} Size: #{size.width}x#{size.height}",
118
123
  @tui.text_line(spans: [
119
124
  @tui.text_span(content: "Geometry Transformations:", style: @label_style),
120
125
  ]),
121
126
  " inner(2): x:#{inner_r.x} y:#{inner_r.y} w:#{inner_r.width} h:#{inner_r.height}",
122
127
  " offset(3,2): x:#{offset_r.x} y:#{offset_r.y} clamp: x:#{clamped.x} y:#{clamped.y}",
123
128
  " union(sidebar): w:#{union_r.width} h:#{union_r.height}",
124
- @tui.text_line(spans: [
125
- @tui.text_span(content: "Iterators:", style: @label_style),
126
- ]),
127
- " rows:#{r.height} columns:#{r.width} positions:#{r.area}",
128
129
  @tui.text_line(spans: [
129
130
  @tui.text_span(content: "Hit Testing ", style: @label_style),
130
131
  @tui.text_span(content: "(Rect#contains?):", style: @dim_style),