ratatui_ruby 1.3.0 → 1.3.3

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 (301) 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/color.rs +1 -1
  5. data/ext/ratatui_ruby/src/errors.rs +1 -1
  6. data/ext/ratatui_ruby/src/events.rs +158 -19
  7. data/ext/ratatui_ruby/src/frame.rs +1 -1
  8. data/ext/ratatui_ruby/src/lib.rs +1 -1
  9. data/ext/ratatui_ruby/src/lib_header.rs +1 -1
  10. data/ext/ratatui_ruby/src/rendering.rs +1 -1
  11. data/ext/ratatui_ruby/src/string_width.rs +1 -1
  12. data/ext/ratatui_ruby/src/style.rs +1 -1
  13. data/ext/ratatui_ruby/src/terminal/capabilities.rs +1 -1
  14. data/ext/ratatui_ruby/src/terminal/init.rs +1 -1
  15. data/ext/ratatui_ruby/src/terminal/mod.rs +1 -1
  16. data/ext/ratatui_ruby/src/terminal/mutations.rs +1 -1
  17. data/ext/ratatui_ruby/src/terminal/queries.rs +1 -1
  18. data/ext/ratatui_ruby/src/terminal/query.rs +1 -1
  19. data/ext/ratatui_ruby/src/terminal/storage.rs +1 -1
  20. data/ext/ratatui_ruby/src/terminal/wrapper.rs +1 -1
  21. data/ext/ratatui_ruby/src/text.rs +1 -1
  22. data/ext/ratatui_ruby/src/widgets/barchart.rs +1 -1
  23. data/ext/ratatui_ruby/src/widgets/block.rs +1 -1
  24. data/ext/ratatui_ruby/src/widgets/calendar.rs +1 -1
  25. data/ext/ratatui_ruby/src/widgets/canvas.rs +1 -1
  26. data/ext/ratatui_ruby/src/widgets/center.rs +1 -1
  27. data/ext/ratatui_ruby/src/widgets/chart.rs +1 -1
  28. data/ext/ratatui_ruby/src/widgets/clear.rs +1 -1
  29. data/ext/ratatui_ruby/src/widgets/cursor.rs +1 -1
  30. data/ext/ratatui_ruby/src/widgets/gauge.rs +1 -1
  31. data/ext/ratatui_ruby/src/widgets/layout.rs +1 -1
  32. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +1 -1
  33. data/ext/ratatui_ruby/src/widgets/list.rs +1 -1
  34. data/ext/ratatui_ruby/src/widgets/list_state.rs +1 -1
  35. data/ext/ratatui_ruby/src/widgets/mod.rs +1 -1
  36. data/ext/ratatui_ruby/src/widgets/overlay.rs +1 -1
  37. data/ext/ratatui_ruby/src/widgets/paragraph.rs +1 -1
  38. data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +1 -1
  39. data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +1 -1
  40. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +1 -1
  41. data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +1 -1
  42. data/ext/ratatui_ruby/src/widgets/sparkline.rs +1 -1
  43. data/ext/ratatui_ruby/src/widgets/table.rs +1 -1
  44. data/ext/ratatui_ruby/src/widgets/table_state.rs +1 -1
  45. data/ext/ratatui_ruby/src/widgets/tabs.rs +1 -1
  46. data/lib/ratatui_ruby/version.rb +1 -1
  47. metadata +1 -255
  48. data/.builds/ruby-3.2.yml +0 -54
  49. data/.builds/ruby-3.3.yml +0 -54
  50. data/.builds/ruby-3.4.yml +0 -54
  51. data/.builds/ruby-4.0.0.yml +0 -54
  52. data/.pre-commit-config.yaml +0 -16
  53. data/.rubocop.yml +0 -10
  54. data/AGENTS.md +0 -147
  55. data/CHANGELOG.md +0 -771
  56. data/README.md +0 -187
  57. data/README.rdoc +0 -302
  58. data/Rakefile +0 -11
  59. data/Steepfile +0 -50
  60. data/doc/concepts/application_architecture.md +0 -321
  61. data/doc/concepts/application_testing.md +0 -193
  62. data/doc/concepts/async.md +0 -190
  63. data/doc/concepts/custom_widgets.md +0 -247
  64. data/doc/concepts/debugging.md +0 -401
  65. data/doc/concepts/event_handling.md +0 -162
  66. data/doc/concepts/interactive_design.md +0 -146
  67. data/doc/contributors/auditing/parity.md +0 -239
  68. data/doc/contributors/design/ruby_frontend.md +0 -448
  69. data/doc/contributors/design/rust_backend.md +0 -434
  70. data/doc/contributors/design.md +0 -11
  71. data/doc/contributors/developing_examples.md +0 -400
  72. data/doc/contributors/documentation_style.md +0 -121
  73. data/doc/contributors/index.md +0 -21
  74. data/doc/contributors/releasing.md +0 -215
  75. data/doc/contributors/todo/align/api_completeness_audit-finished.md +0 -381
  76. data/doc/contributors/todo/align/api_completeness_audit-unfinished.md +0 -200
  77. data/doc/contributors/todo/align/term.md +0 -351
  78. data/doc/contributors/todo/align/terminal.md +0 -647
  79. data/doc/contributors/todo/future_work.md +0 -169
  80. data/doc/contributors/upstream_requests/paragraph_span_rects.md +0 -259
  81. data/doc/contributors/upstream_requests/tab_rects.md +0 -173
  82. data/doc/contributors/upstream_requests/title_rects.md +0 -132
  83. data/doc/custom.css +0 -22
  84. data/doc/getting_started/quickstart.md +0 -291
  85. data/doc/getting_started/why.md +0 -93
  86. data/doc/images/app_all_events.png +0 -0
  87. data/doc/images/app_cli_rich_moments.gif +0 -0
  88. data/doc/images/app_color_picker.png +0 -0
  89. data/doc/images/app_debugging_showcase.gif +0 -0
  90. data/doc/images/app_debugging_showcase.png +0 -0
  91. data/doc/images/app_external_editor.gif +0 -0
  92. data/doc/images/app_login_form.png +0 -0
  93. data/doc/images/app_stateful_interaction.png +0 -0
  94. data/doc/images/verify_quickstart_dsl.png +0 -0
  95. data/doc/images/verify_quickstart_layout.png +0 -0
  96. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  97. data/doc/images/verify_readme_usage.png +0 -0
  98. data/doc/images/widget_barchart.png +0 -0
  99. data/doc/images/widget_block.png +0 -0
  100. data/doc/images/widget_box.png +0 -0
  101. data/doc/images/widget_calendar.png +0 -0
  102. data/doc/images/widget_canvas.png +0 -0
  103. data/doc/images/widget_cell.png +0 -0
  104. data/doc/images/widget_center.png +0 -0
  105. data/doc/images/widget_chart.png +0 -0
  106. data/doc/images/widget_gauge.png +0 -0
  107. data/doc/images/widget_layout_split.png +0 -0
  108. data/doc/images/widget_line_gauge.png +0 -0
  109. data/doc/images/widget_list.png +0 -0
  110. data/doc/images/widget_map.png +0 -0
  111. data/doc/images/widget_overlay.png +0 -0
  112. data/doc/images/widget_popup.png +0 -0
  113. data/doc/images/widget_ratatui_logo.png +0 -0
  114. data/doc/images/widget_ratatui_mascot.png +0 -0
  115. data/doc/images/widget_rect.png +0 -0
  116. data/doc/images/widget_render.png +0 -0
  117. data/doc/images/widget_rich_text.png +0 -0
  118. data/doc/images/widget_scroll_text.png +0 -0
  119. data/doc/images/widget_scrollbar.png +0 -0
  120. data/doc/images/widget_sparkline.png +0 -0
  121. data/doc/images/widget_style_colors.png +0 -0
  122. data/doc/images/widget_table.png +0 -0
  123. data/doc/images/widget_tabs.png +0 -0
  124. data/doc/images/widget_text_width.png +0 -0
  125. data/doc/index.md +0 -34
  126. data/doc/troubleshooting/async.md +0 -4
  127. data/doc/troubleshooting/terminal_limitations.md +0 -131
  128. data/doc/troubleshooting/tui_output.md +0 -197
  129. data/examples/app_all_events/README.md +0 -114
  130. data/examples/app_all_events/app.rb +0 -98
  131. data/examples/app_all_events/model/app_model.rb +0 -159
  132. data/examples/app_all_events/model/event_color_cycle.rb +0 -43
  133. data/examples/app_all_events/model/event_entry.rb +0 -94
  134. data/examples/app_all_events/model/msg.rb +0 -39
  135. data/examples/app_all_events/model/timestamp.rb +0 -56
  136. data/examples/app_all_events/update.rb +0 -75
  137. data/examples/app_all_events/view/app_view.rb +0 -80
  138. data/examples/app_all_events/view/controls_view.rb +0 -54
  139. data/examples/app_all_events/view/counts_view.rb +0 -61
  140. data/examples/app_all_events/view/live_view.rb +0 -72
  141. data/examples/app_all_events/view/log_view.rb +0 -57
  142. data/examples/app_all_events/view.rb +0 -9
  143. data/examples/app_cli_rich_moments/README.md +0 -81
  144. data/examples/app_cli_rich_moments/app.rb +0 -189
  145. data/examples/app_color_picker/README.md +0 -156
  146. data/examples/app_color_picker/app.rb +0 -76
  147. data/examples/app_color_picker/clipboard.rb +0 -86
  148. data/examples/app_color_picker/color.rb +0 -193
  149. data/examples/app_color_picker/controls.rb +0 -92
  150. data/examples/app_color_picker/copy_dialog.rb +0 -168
  151. data/examples/app_color_picker/export_pane.rb +0 -128
  152. data/examples/app_color_picker/harmony.rb +0 -58
  153. data/examples/app_color_picker/input.rb +0 -176
  154. data/examples/app_color_picker/main_container.rb +0 -180
  155. data/examples/app_color_picker/palette.rb +0 -111
  156. data/examples/app_debugging_showcase/README.md +0 -119
  157. data/examples/app_debugging_showcase/app.rb +0 -318
  158. data/examples/app_external_editor/README.md +0 -62
  159. data/examples/app_external_editor/app.rb +0 -344
  160. data/examples/app_login_form/README.md +0 -58
  161. data/examples/app_login_form/app.rb +0 -109
  162. data/examples/app_stateful_interaction/README.md +0 -35
  163. data/examples/app_stateful_interaction/app.rb +0 -328
  164. data/examples/timeout_demo.rb +0 -45
  165. data/examples/verify_quickstart_dsl/README.md +0 -55
  166. data/examples/verify_quickstart_dsl/app.rb +0 -49
  167. data/examples/verify_quickstart_layout/README.md +0 -77
  168. data/examples/verify_quickstart_layout/app.rb +0 -73
  169. data/examples/verify_quickstart_lifecycle/README.md +0 -68
  170. data/examples/verify_quickstart_lifecycle/app.rb +0 -62
  171. data/examples/verify_readme_usage/README.md +0 -49
  172. data/examples/verify_readme_usage/app.rb +0 -42
  173. data/examples/verify_website_managed/README.md +0 -48
  174. data/examples/verify_website_managed/app.rb +0 -36
  175. data/examples/verify_website_menu/README.md +0 -60
  176. data/examples/verify_website_menu/app.rb +0 -84
  177. data/examples/verify_website_spinner/README.md +0 -44
  178. data/examples/verify_website_spinner/app.rb +0 -34
  179. data/examples/widget_barchart/README.md +0 -58
  180. data/examples/widget_barchart/app.rb +0 -240
  181. data/examples/widget_block/README.md +0 -44
  182. data/examples/widget_block/app.rb +0 -258
  183. data/examples/widget_box/README.md +0 -54
  184. data/examples/widget_box/app.rb +0 -255
  185. data/examples/widget_calendar/README.md +0 -48
  186. data/examples/widget_calendar/app.rb +0 -115
  187. data/examples/widget_canvas/README.md +0 -31
  188. data/examples/widget_canvas/app.rb +0 -130
  189. data/examples/widget_cell/README.md +0 -45
  190. data/examples/widget_cell/app.rb +0 -112
  191. data/examples/widget_center/README.md +0 -33
  192. data/examples/widget_center/app.rb +0 -118
  193. data/examples/widget_chart/README.md +0 -50
  194. data/examples/widget_chart/app.rb +0 -220
  195. data/examples/widget_gauge/README.md +0 -50
  196. data/examples/widget_gauge/app.rb +0 -229
  197. data/examples/widget_layout_split/README.md +0 -53
  198. data/examples/widget_layout_split/app.rb +0 -260
  199. data/examples/widget_line_gauge/README.md +0 -50
  200. data/examples/widget_line_gauge/app.rb +0 -219
  201. data/examples/widget_list/README.md +0 -58
  202. data/examples/widget_list/app.rb +0 -382
  203. data/examples/widget_map/README.md +0 -48
  204. data/examples/widget_map/app.rb +0 -95
  205. data/examples/widget_overlay/README.md +0 -45
  206. data/examples/widget_overlay/app.rb +0 -250
  207. data/examples/widget_popup/README.md +0 -45
  208. data/examples/widget_popup/app.rb +0 -106
  209. data/examples/widget_ratatui_logo/README.md +0 -43
  210. data/examples/widget_ratatui_logo/app.rb +0 -104
  211. data/examples/widget_ratatui_mascot/README.md +0 -43
  212. data/examples/widget_ratatui_mascot/app.rb +0 -95
  213. data/examples/widget_rect/README.md +0 -53
  214. data/examples/widget_rect/app.rb +0 -222
  215. data/examples/widget_render/README.md +0 -46
  216. data/examples/widget_render/app.rb +0 -186
  217. data/examples/widget_render/app.rbs +0 -41
  218. data/examples/widget_rich_text/README.md +0 -44
  219. data/examples/widget_rich_text/app.rb +0 -193
  220. data/examples/widget_scroll_text/README.md +0 -46
  221. data/examples/widget_scroll_text/app.rb +0 -109
  222. data/examples/widget_scrollbar/README.md +0 -46
  223. data/examples/widget_scrollbar/app.rb +0 -155
  224. data/examples/widget_sparkline/README.md +0 -51
  225. data/examples/widget_sparkline/app.rb +0 -277
  226. data/examples/widget_style_colors/README.md +0 -43
  227. data/examples/widget_style_colors/app.rb +0 -83
  228. data/examples/widget_table/README.md +0 -57
  229. data/examples/widget_table/app.rb +0 -285
  230. data/examples/widget_tabs/README.md +0 -50
  231. data/examples/widget_tabs/app.rb +0 -183
  232. data/examples/widget_text_width/README.md +0 -44
  233. data/examples/widget_text_width/app.rb +0 -117
  234. data/migrate_to_buffer.rb +0 -145
  235. data/mise.toml +0 -8
  236. data/tasks/autodoc/examples.rb +0 -87
  237. data/tasks/autodoc/member.rb +0 -58
  238. data/tasks/autodoc/name.rb +0 -21
  239. data/tasks/autodoc.rake +0 -21
  240. data/tasks/bump/bump_workflow.rb +0 -49
  241. data/tasks/bump/cargo_lockfile.rb +0 -21
  242. data/tasks/bump/changelog.rb +0 -104
  243. data/tasks/bump/header.rb +0 -32
  244. data/tasks/bump/history.rb +0 -32
  245. data/tasks/bump/links.rb +0 -69
  246. data/tasks/bump/manifest.rb +0 -33
  247. data/tasks/bump/patch_release.rb +0 -19
  248. data/tasks/bump/release_branch.rb +0 -17
  249. data/tasks/bump/release_from_trunk.rb +0 -49
  250. data/tasks/bump/repository.rb +0 -54
  251. data/tasks/bump/ruby_gem.rb +0 -29
  252. data/tasks/bump/sem_ver.rb +0 -44
  253. data/tasks/bump/unreleased_section.rb +0 -73
  254. data/tasks/bump.rake +0 -61
  255. data/tasks/doc/documentation.rb +0 -59
  256. data/tasks/doc/link/file_url.rb +0 -30
  257. data/tasks/doc/link/relative_path.rb +0 -61
  258. data/tasks/doc/link/web_url.rb +0 -55
  259. data/tasks/doc/link.rb +0 -52
  260. data/tasks/doc/link_audit.rb +0 -116
  261. data/tasks/doc/problem.rb +0 -40
  262. data/tasks/doc/source_file.rb +0 -93
  263. data/tasks/doc.rake +0 -905
  264. data/tasks/example_viewer.html.erb +0 -172
  265. data/tasks/extension.rake +0 -14
  266. data/tasks/license/headers_md.rb +0 -223
  267. data/tasks/license/headers_rb.rb +0 -210
  268. data/tasks/license/license_utils.rb +0 -130
  269. data/tasks/license/snippets_md.rb +0 -315
  270. data/tasks/license/snippets_rdoc.rb +0 -150
  271. data/tasks/license.rake +0 -91
  272. data/tasks/lint.rake +0 -170
  273. data/tasks/rbs_predicates/predicate_catalog.rb +0 -52
  274. data/tasks/rbs_predicates/predicate_tests.rb +0 -124
  275. data/tasks/rbs_predicates/rbs_signature.rb +0 -63
  276. data/tasks/rbs_predicates.rake +0 -31
  277. data/tasks/rdoc_config.rb +0 -29
  278. data/tasks/resources/build.yml.erb +0 -60
  279. data/tasks/resources/index.html.erb +0 -141
  280. data/tasks/resources/rubies.yml +0 -7
  281. data/tasks/sourcehut.rake +0 -122
  282. data/tasks/steep.rake +0 -11
  283. data/tasks/terminal_preview/app_screenshot.rb +0 -45
  284. data/tasks/terminal_preview/crash_report.rb +0 -54
  285. data/tasks/terminal_preview/example_app.rb +0 -27
  286. data/tasks/terminal_preview/launcher_script.rb +0 -48
  287. data/tasks/terminal_preview/preview_collection.rb +0 -60
  288. data/tasks/terminal_preview/preview_timing.rb +0 -24
  289. data/tasks/terminal_preview/safety_confirmation.rb +0 -58
  290. data/tasks/terminal_preview/saved_screenshot.rb +0 -56
  291. data/tasks/terminal_preview/system_appearance.rb +0 -13
  292. data/tasks/terminal_preview/terminal_window.rb +0 -138
  293. data/tasks/terminal_preview/window_id.rb +0 -16
  294. data/tasks/terminal_preview.rake +0 -30
  295. data/tasks/test.rake +0 -36
  296. data/tasks/website/index_page.rb +0 -30
  297. data/tasks/website/version.rb +0 -122
  298. data/tasks/website/version_menu.rb +0 -68
  299. data/tasks/website/versioned_documentation.rb +0 -83
  300. data/tasks/website/website.rb +0 -53
  301. data/tasks/website.rake +0 -28
@@ -1,344 +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
- $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
9
- require "ratatui_ruby"
10
- require "tempfile"
11
-
12
- # Disable experimental warnings for line_count
13
- RatatuiRuby.experimental_warnings = false
14
-
15
- # External Editor Example
16
- #
17
- # Demonstrates full lifecycle control for temporarily exiting the TUI
18
- # to invoke an external editor, then re-entering—like editing a commit
19
- # message in lazygit.
20
- #
21
- # This example uses the low-level API (init_terminal/restore_terminal)
22
- # instead of RatatuiRuby.run, which is required when you need to
23
- # repeatedly enter and exit raw mode during a session.
24
- #
25
- # Features:
26
- # - `e` edits this example's README.md
27
- # - `s` edits a scratch file; saved content appears in a split pane
28
- # - Focus-aware scrolling with visual indicators
29
- # - Mouse scroll on hover
30
- class AppExternalEditor
31
- README_PATH = File.expand_path("README.md", __dir__)
32
-
33
- def initialize
34
- @tui = RatatuiRuby::TUI.new
35
- @readme_scroll = 0
36
- @scratch_scroll = 0
37
- @readme_content = File.read(README_PATH)
38
- @scratch = Tempfile.new(["scratch", ".md"])
39
- @scratch_content = nil
40
- @focus = :readme
41
-
42
- # Cached geometry (set during calculate_layout)
43
- @readme_area = nil
44
- @scratch_area = nil
45
- @readme_page_height = 10
46
- @scratch_page_height = 10
47
- @readme_line_count = 0
48
- @scratch_line_count = 0
49
- end
50
-
51
- def run
52
- loop do
53
- action = tui_session
54
- case action
55
- when :quit then break
56
- when :edit_readme then edit_file(README_PATH) { reload_readme }
57
- when :edit_scratch then edit_file(@scratch.path) { reload_scratch }
58
- end
59
- end
60
- ensure
61
- @scratch.unlink
62
- end
63
-
64
- private def tui_session
65
- RatatuiRuby.init_terminal
66
- begin
67
- loop do
68
- @tui.draw do |frame|
69
- calculate_layout(frame.area) # Phase 1: Geometry (once per frame)
70
- render(frame) # Phase 2: Draw
71
- end
72
- action = handle_input # Phase 3: Input (uses cached geometry)
73
- return action if action # Early return triggers ensure block
74
- end
75
- ensure
76
- RatatuiRuby.restore_terminal # Always runs, even on early return
77
- end
78
- end
79
-
80
- # Open an external editor on the given file.
81
- #
82
- # Note: The terminal is already restored when this method is called!
83
- # The tui_session method's ensure block handles restore_terminal before
84
- # we get here. After the editor closes, the run loop calls tui_session
85
- # again which does init_terminal. This pattern avoids explicit
86
- # save/restore calls at every handoff point.
87
- private def edit_file(path)
88
- editor = ENV.fetch("EDITOR", "vi")
89
- system(editor, path)
90
- yield if block_given?
91
- end
92
-
93
- # Phase 1: Calculate layout and cache all geometry
94
- private def calculate_layout(area)
95
- content_area, @controls_area = @tui.layout_split(
96
- area,
97
- direction: :vertical,
98
- constraints: [
99
- @tui.constraint_fill(1),
100
- @tui.constraint_length(3),
101
- ]
102
- )
103
-
104
- if split_view?
105
- @readme_area, @scratch_area = @tui.layout_split(
106
- content_area,
107
- direction: :horizontal,
108
- constraints: [
109
- @tui.constraint_percentage(50),
110
- @tui.constraint_percentage(50),
111
- ]
112
- )
113
- else
114
- @readme_area = content_area
115
- @scratch_area = nil
116
- end
117
-
118
- # Calculate wrapped line counts and page heights
119
- calculate_readme_geometry
120
- calculate_scratch_geometry if split_view?
121
-
122
- # Clamp scroll positions now that we have accurate geometry
123
- clamp_scroll_positions
124
- end
125
-
126
- private def calculate_readme_geometry
127
- block = @tui.block(borders: [:all])
128
- inner = block.inner(@readme_area)
129
- @readme_page_height = inner.height
130
-
131
- paragraph = @tui.paragraph(text: @readme_content, wrap: true, block:)
132
- @readme_line_count = paragraph.line_count(inner.width)
133
- end
134
-
135
- private def calculate_scratch_geometry
136
- return unless @scratch_area
137
-
138
- block = @tui.block(borders: [:all])
139
- inner = block.inner(@scratch_area)
140
- @scratch_page_height = inner.height
141
-
142
- paragraph = @tui.paragraph(text: @scratch_content || "", wrap: true, block:)
143
- @scratch_line_count = paragraph.line_count(inner.width)
144
- end
145
-
146
- private def clamp_scroll_positions
147
- @readme_scroll = @readme_scroll.clamp(0, max_readme_scroll)
148
- @scratch_scroll = @scratch_scroll.clamp(0, max_scratch_scroll) if split_view?
149
- end
150
-
151
- # Phase 2: Render using cached geometry
152
- private def render(frame)
153
- render_readme(frame)
154
- render_scratch(frame) if split_view?
155
- render_controls(frame)
156
- end
157
-
158
- private def split_view? = @scratch_content && !@scratch_content.strip.empty?
159
-
160
- private def render_readme(frame)
161
- focused = @focus == :readme
162
- paragraph = @tui.paragraph(
163
- text: @readme_content,
164
- scroll: [@readme_scroll, 0],
165
- wrap: true,
166
- block: @tui.block(
167
- title: "README.md (#{@readme_scroll + 1}-#{[@readme_scroll + @readme_page_height, @readme_line_count].min}/#{@readme_line_count})",
168
- borders: [:all],
169
- border_style: focused ? { fg: "cyan" } : { fg: "dark_gray" }
170
- )
171
- )
172
- frame.render_widget(paragraph, @readme_area)
173
-
174
- # Stateful scrollbar with viewport_content_length
175
- # content_length = max_scroll + 1, so position=max_scroll => content_length-1 => thumb at bottom
176
- scrollbar = @tui.scrollbar(content_length: 0, position: 0, orientation: :vertical, track_symbol: nil)
177
- state = RatatuiRuby::ScrollbarState.new(max_readme_scroll + 1)
178
- state.position = @readme_scroll
179
- state.viewport_content_length = @readme_page_height
180
- frame.render_stateful_widget(scrollbar, @readme_area, state)
181
- end
182
-
183
- private def render_scratch(frame)
184
- focused = @focus == :scratch
185
- paragraph = @tui.paragraph(
186
- text: @scratch_content,
187
- scroll: [@scratch_scroll, 0],
188
- wrap: true,
189
- block: @tui.block(
190
- title: "Scratch (#{@scratch_scroll + 1}-#{[@scratch_scroll + @scratch_page_height, @scratch_line_count].min}/#{@scratch_line_count})",
191
- borders: [:all],
192
- border_style: focused ? { fg: "yellow" } : { fg: "dark_gray" }
193
- )
194
- )
195
- frame.render_widget(paragraph, @scratch_area)
196
-
197
- # Stateful scrollbar with viewport_content_length
198
- scrollbar = @tui.scrollbar(content_length: 0, position: 0, orientation: :vertical, track_symbol: nil)
199
- state = RatatuiRuby::ScrollbarState.new(max_scratch_scroll + 1)
200
- state.position = @scratch_scroll
201
- state.viewport_content_length = @scratch_page_height
202
- frame.render_stateful_widget(scrollbar, @scratch_area, state)
203
- end
204
-
205
- private def render_controls(frame)
206
- spans = [
207
- @tui.text_span(content: "↑/↓", style: hotkey_style),
208
- @tui.text_span(content: ": Scroll "),
209
- @tui.text_span(content: "e", style: hotkey_style),
210
- @tui.text_span(content: ": Edit README "),
211
- @tui.text_span(content: "s", style: hotkey_style),
212
- @tui.text_span(content: ": Edit Scratch "),
213
- ]
214
- if split_view?
215
- spans += [
216
- @tui.text_span(content: "Tab", style: hotkey_style),
217
- @tui.text_span(content: ": Focus "),
218
- ]
219
- end
220
- spans += [
221
- @tui.text_span(content: "q", style: hotkey_style),
222
- @tui.text_span(content: ": Quit"),
223
- ]
224
-
225
- paragraph = @tui.paragraph(
226
- text: [@tui.text_line(spans:)],
227
- alignment: :center,
228
- block: @tui.block(title: "Controls", borders: [:all])
229
- )
230
- frame.render_widget(paragraph, @controls_area)
231
- end
232
-
233
- private def hotkey_style = @tui.style(modifiers: [:bold, :underlined])
234
-
235
- # Phase 3: Handle input using cached geometry
236
- private def handle_input
237
- case @tui.poll_event
238
- # Quit
239
- in { type: :key, code: "q" } | { type: :key, code: "c", modifiers: ["ctrl"] }
240
- :quit
241
-
242
- # Edit actions
243
- in { type: :key, code: "e" }
244
- :edit_readme
245
- in { type: :key, code: "s" }
246
- :edit_scratch
247
-
248
- # Focus switching (only in split view)
249
- in { type: :key, code: "tab" } | { type: :key, code: "backtab" } |
250
- { type: :key, code: "left" } | { type: :key, code: "right" } |
251
- { type: :key, code: "h" } | { type: :key, code: "l" } if split_view?
252
- switch_focus
253
- nil
254
-
255
- # Keyboard scrolling
256
- in { type: :key, code: "up" } | { type: :key, code: "k" }
257
- scroll_up
258
- nil
259
- in { type: :key, code: "down" } | { type: :key, code: "j" }
260
- scroll_down
261
- nil
262
- in { type: :key, code: "page_up" }
263
- scroll_up(page_height)
264
- nil
265
- in { type: :key, code: "page_down" }
266
- scroll_down(page_height)
267
- nil
268
- in { type: :key, code: "home" }
269
- scroll_to(0)
270
- nil
271
- in { type: :key, code: "end" }
272
- scroll_to(max_scroll)
273
- nil
274
-
275
- # Mouse scroll - hit test to determine target pane
276
- in { type: :mouse, kind: "scroll_up", x:, y: }
277
- pane = pane_at(x, y)
278
- scroll_pane_up(pane) if pane
279
- nil
280
- in { type: :mouse, kind: "scroll_down", x:, y: }
281
- pane = pane_at(x, y)
282
- scroll_pane_down(pane) if pane
283
- nil
284
-
285
- # Mouse click to focus (only in split view)
286
- in { type: :mouse, kind: "down", x:, y: } if split_view?
287
- pane = pane_at(x, y)
288
- @focus = pane if pane
289
- nil
290
-
291
- else
292
- nil
293
- end
294
- end
295
-
296
- private def pane_at(x, y)
297
- if @scratch_area&.contains?(x, y)
298
- :scratch
299
- elsif @readme_area&.contains?(x, y)
300
- :readme
301
- end
302
- end
303
-
304
- private def switch_focus
305
- @focus = (@focus == :readme) ? :scratch : :readme
306
- end
307
-
308
- private def scroll_up(n = 1) = scroll_pane_up(@focus, n)
309
- private def scroll_down(n = 1) = scroll_pane_down(@focus, n)
310
-
311
- private def scroll_pane_up(pane, n = 1)
312
- if pane == :readme
313
- @readme_scroll = [@readme_scroll - n, 0].max
314
- else
315
- @scratch_scroll = [@scratch_scroll - n, 0].max
316
- end
317
- end
318
-
319
- private def scroll_pane_down(pane, n = 1)
320
- if pane == :readme
321
- @readme_scroll = [@readme_scroll + n, max_readme_scroll].min
322
- else
323
- @scratch_scroll = [@scratch_scroll + n, max_scratch_scroll].min
324
- end
325
- end
326
-
327
- private def scroll_to(y)
328
- if @focus == :readme
329
- @readme_scroll = y.clamp(0, max_readme_scroll)
330
- else
331
- @scratch_scroll = y.clamp(0, max_scratch_scroll)
332
- end
333
- end
334
-
335
- private def max_scroll = (@focus == :readme) ? max_readme_scroll : max_scratch_scroll
336
- private def max_readme_scroll = [@readme_line_count - @readme_page_height, 0].max
337
- private def max_scratch_scroll = [@scratch_line_count - @scratch_page_height, 0].max
338
- private def page_height = (@focus == :readme) ? @readme_page_height : @scratch_page_height
339
-
340
- private def reload_readme = @readme_content = File.read(README_PATH)
341
- private def reload_scratch = @scratch_content = File.read(@scratch.path)
342
- end
343
-
344
- AppExternalEditor.new.run if __FILE__ == $PROGRAM_NAME
@@ -1,58 +0,0 @@
1
- <!--
2
- SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
- SPDX-License-Identifier: CC-BY-SA-4.0
4
- -->
5
-
6
- # Login Form Example
7
-
8
- [![Login Form](../../doc/images/app_login_form.png)](app.rb)
9
-
10
- Demonstrates how to create a modal overlay for user input.
11
-
12
- Many applications need to block interaction with the main UI while collecting specific information, like a login prompt or confirmation dialog. Managing the z-index and input focus for these overlays can be tricky.
13
-
14
- This example solves this by using the `Overlay` widget to stack a centered popup on top of a base layer, conditionally rendering the popup based on state.
15
-
16
- ## Features Demonstrated
17
-
18
- - **Overlays:** Stacking widgets on top of each other using `tui.overlay`.
19
- - **Centering:** Positioning a widget in the center of the screen using `tui.center`.
20
- - **State Management:** Switching between "Base" and "Popup" views.
21
- - **Input Handling:** Capturing text input and handling specific keys (Enter, Esc) to trigger state changes.
22
- - **Cursor Positioning:** Manually calculating cursor position within a `Paragraph`.
23
-
24
- ## Hotkeys
25
-
26
- ### Form Mode
27
- - **Text Input**: Type to enter username (supports all characters including 'q').
28
- - **Backspace**: Deletes the last character.
29
- - **Enter**: Submits the form and opens the success popup.
30
- - **Esc**: Quits the application.
31
- - **Ctrl+C**: Quits the application.
32
-
33
- ### Popup Mode
34
- - **q**: Closes the popup and quits the application.
35
- - **Ctrl+C**: Quits the application.
36
-
37
- ## Usage
38
-
39
- <!-- SPDX-SnippetBegin -->
40
- <!--
41
- SPDX-FileCopyrightText: 2026 Kerrick Long
42
- SPDX-License-Identifier: MIT-0
43
- -->
44
- ```bash
45
- ruby examples/app_login_form/app.rb
46
- ```
47
- <!-- SPDX-SnippetEnd -->
48
-
49
- ## Learning Outcomes
50
-
51
- Use this example if you need to...
52
-
53
- - Create a modal dialog or popup.
54
- - Center a widget on the screen (vertically and horizontally).
55
- - Implement a simple text input field with cursor management.
56
- - layer widgets using the `Overlay` widget.
57
-
58
- [Read the source code →](app.rb)
@@ -1,109 +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
- $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
9
- require "ratatui_ruby"
10
-
11
- class AppLoginForm
12
- PREFIX = "Enter Username: [ "
13
- SUFFIX = " ]"
14
-
15
- def initialize
16
- @username = ""
17
- @show_popup = false
18
- end
19
-
20
- def run
21
- RatatuiRuby.run do |tui|
22
- @tui = tui
23
- loop do
24
- render
25
- break if handle_input == :quit
26
- end
27
- end
28
- end
29
-
30
- private def render
31
- # 1. Base Layer Construction
32
- # We want a cursor relative to the paragraph.
33
- # So we wrap Paragraph and Cursor in an Overlay, and put that Overlay in a Center.
34
-
35
- # Calculate cursor position
36
- # Border takes 1 cell.
37
- # Cursor X = 1 (border) + PREFIX.length + username.length
38
- # Cursor Y = 1 (border + line 0)
39
- cursor_x = 1 + PREFIX.length + @username.length
40
- cursor_y = 1
41
-
42
- # The content of the base form
43
- form_content = @tui.overlay(layers: [
44
- @tui.paragraph(
45
- text: "#{PREFIX}#{@username}#{SUFFIX}",
46
- block: @tui.block(borders: :all, title: "Login Form"),
47
- alignment: :left
48
- ),
49
- @tui.cursor(x: cursor_x, y: cursor_y),
50
- ])
51
-
52
- # Center the form on screen
53
- base_layer = @tui.center(
54
- child: form_content,
55
- width_percent: 50,
56
- height_percent: 20
57
- )
58
-
59
- # 2. Popup Layer Construction
60
- final_view = if @show_popup
61
- popup_message = @tui.center(
62
- child: @tui.paragraph(
63
- text: "Login Successful!\nPress 'q' to quit.",
64
- style: @tui.style(fg: :green, bg: :black),
65
- block: @tui.block(borders: :all),
66
- alignment: :center,
67
- wrap: true
68
- ),
69
- width_percent: 30,
70
- height_percent: 20
71
- )
72
-
73
- # Render Base Layer (background) THEN Popup Layer
74
- @tui.overlay(layers: [base_layer, popup_message])
75
- else
76
- base_layer
77
- end
78
-
79
- # 3. Draw
80
- @tui.draw do |frame|
81
- frame.render_widget(final_view, frame.area)
82
- end
83
- end
84
-
85
- private def handle_input
86
- case @tui.poll_event
87
- in { type: :key, code: "c", modifiers: ["ctrl"] }
88
- :quit
89
- in { type: :key, code: "q" } if @show_popup
90
- :quit
91
- in { type: :key, code: "enter" }
92
- @show_popup ||= true
93
- nil
94
- in { type: :key, code: "backspace" }
95
- @username.chop! unless @show_popup
96
- nil
97
- in { type: :key, code: "esc" }
98
- :quit unless @show_popup
99
- in { type: :key, code:, modifiers: [] }
100
- # Simple text input (single character, no modifiers)
101
- @username += code if !@show_popup && code.length == 1
102
- nil
103
- else
104
- nil
105
- end
106
- end
107
- end
108
-
109
- AppLoginForm.new.run if __FILE__ == $0
@@ -1,35 +0,0 @@
1
- <!--
2
- SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
- SPDX-License-Identifier: CC-BY-SA-4.0
4
- -->
5
-
6
- # Stateful Interaction Example
7
-
8
- [![Stateful Interaction](../../doc/images/app_stateful_interaction.png)](app.rb)
9
-
10
- This example demonstrates High-Fidelity Interaction using **Stateful Widget Rendering**.
11
-
12
- It showcases a "Database Viewer" layout where:
13
- 1. **Selection Persistence:** `ListState` and `TableState` objects persist across frames, maintaining selection without manual index tracking variables.
14
- 2. **Offset Read-back:** The application reads `state.offset` *after* rendering to know exactly which items were visible on screen.
15
- 3. **Mouse Interaction:** Using the read-back offset, we can calculate exactly which row was clicked, even when the specific item wasn't drawn at that absolute Y position due to scrolling.
16
-
17
- ## Key Concept: The "Read-back" Loop
18
-
19
- Standard immediate-mode interaction often requires you to re-calculate layout logic to determine what was clicked.
20
-
21
- In `ratatui_ruby`'s Stateful Rendering:
22
- 1. **Update**: You modify `state` (e.g., `state.select(1)`).
23
- 2. **Render**: You pass `state` to `render_stateful_widget`. Ratatui's Rust backend calculates layout and **updates** `state.offset` in-place if scrolling happened.
24
- 3. **Interact**: On the next event loop, you use `state.offset` to correctly map mouse coordinates to data indices.
25
-
26
- ## Hotkeys
27
-
28
- | Key | Action |
29
- | --- | --- |
30
- | `↑` / `↓` | Scroll the active pane |
31
- | `Tab` / `←` / `→` | Switch active pane (List vs Table) |
32
- | `Mouse Click` | Select the clicked row (Works with scrolling!) |
33
- | `q` | Quit |
34
-
35
- [Read the source code →](app.rb)