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,37 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
4
+ -->
5
+
6
+ # Scrollbar Widget Example
7
+
8
+ Demonstrates explicit scrollbars for navigation feedback.
9
+
10
+ Content overflows. Users get lost in long lists. Scrollbars provide essential spatial awareness ("How far down am I?") and navigation controls.
11
+
12
+ ## Features Demonstrated
13
+
14
+ - **Orientation**: Vertical, Horizontal, and variation modes (Right/Left, Top/Bottom).
15
+ - **Styling**: Custom characters for Track, Thumb, and arrows.
16
+ - **State Integration**: Linking the scrollbar `position` to the content view state.
17
+
18
+ ## Hotkeys
19
+
20
+ - **Mouse Wheel**: Scroll content (`position`)
21
+ - **s**: Cycle Scrollbar Theme (Standard, Rounded, ASCII, Minimal)
22
+ - **o**: Cycle Orientation (`orientation`)
23
+ - **q**: Quit
24
+
25
+ ## Usage
26
+
27
+ ```bash
28
+ ruby examples/widget_scrollbar_demo/app.rb
29
+ ```
30
+
31
+ ## Learning Outcomes
32
+
33
+ Use this example if you need to...
34
+ - Add visual scroll indicators to Lists or Tables.
35
+ - Implement specialized inputs like sliders or volume controls.
36
+
37
+ ![Demo](/doc/images/widget_scrollbar_demo.png)
@@ -0,0 +1,42 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
4
+ -->
5
+
6
+ # Sparkline Widget Example
7
+
8
+ Demonstrates high-density data visualization in a condensed footprint.
9
+
10
+ Users need context. A single number ("90% CPU") tells you status, but not the trend. Full charts take up too much space. Sparklines condense history into a single line, perfect for headers and dashboards.
11
+
12
+ ## Features Demonstrated
13
+
14
+ - **High Density**: Showing dozens of data points in a small area.
15
+ - **Direction**: Rendering Left-to-Right (standard) or Right-to-Left (like a scrolling ticker).
16
+ - **Gaps**: Handling `nil` values with "absent symbols" to indicate missing data.
17
+ - **Styling**: Using colors and custom characters to indicate severity or type.
18
+
19
+ ## Hotkeys
20
+
21
+ - **Up/Down (↑/↓)**: Cycle Data Set (`data`)
22
+ - **d**: Cycle Direction (`direction`)
23
+ - **c**: Cycle Color (`style`)
24
+ - **m**: Cycle Absent Value Marker Symbol (`absent_value_symbol`)
25
+ - **s**: Cycle Absent Value Marker Style (`absent_value_style`)
26
+ - **b**: Cycle Bar Character Set (`bar_set`)
27
+ - **q**: Quit
28
+
29
+ ## Usage
30
+
31
+ ```bash
32
+ ruby examples/widget_sparkline_demo/app.rb
33
+ ```
34
+
35
+ ## Learning Outcomes
36
+
37
+ Use this example if you need to...
38
+ - Add a "CPU Load" graph to your header.
39
+ - Visualize stock price trends in a list row.
40
+ - Monitor memory usage over the last 60 seconds.
41
+
42
+ ![Demo](/doc/images/widget_sparkline_demo.png)
@@ -57,7 +57,8 @@ class WidgetSparklineDemo
57
57
  data: [1, 5, 1, 8, 1, 6, 1, 9, 1, 7, 1, 10],
58
58
  },
59
59
  ]
60
- @data_index = 0
60
+ @data_index = 2
61
+ srand(12345) # Ensure reproducible "Random" data for snapshots
61
62
 
62
63
  @directions = [
63
64
  { name: "Left to Right", direction: :left_to_right },
@@ -72,7 +73,7 @@ class WidgetSparklineDemo
72
73
  { name: "Cyan", style: @tui.style(fg: :cyan) },
73
74
  { name: "Magenta", style: @tui.style(fg: :magenta) },
74
75
  ]
75
- @style_index = 0
76
+ @style_index = 3
76
77
 
77
78
  @absent_symbols = [
78
79
  { name: "None", symbol: nil },
@@ -89,7 +90,7 @@ class WidgetSparklineDemo
89
90
  { name: "Dim Red", style: @tui.style(fg: :red, modifiers: [:dim]) },
90
91
  { name: "Dim Yellow", style: @tui.style(fg: :yellow, modifiers: [:dim]) },
91
92
  ]
92
- @absent_style_index = 0
93
+ @absent_style_index = 2
93
94
 
94
95
  @bar_sets = [
95
96
  { name: "Default (Block)", set: nil },
@@ -0,0 +1,34 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
4
+ -->
5
+
6
+ # Style Colors Example
7
+
8
+ Demonstrates high-fidelity color support.
9
+
10
+ Terminals support millions of colors. This example generates a mathematically precise HSL gradient to prove the rendering engine's color fidelity.
11
+
12
+ ## Features Demonstrated
13
+
14
+ - **HSL to RGB Conversion**: generating smooth color gradients programmatically.
15
+ - **TrueColor Support**: Rendering arbitrary HEX colors.
16
+
17
+ ## Hotkeys
18
+
19
+ - **q** / **Ctrl+c**: Quit
20
+
21
+ ## Usage
22
+
23
+ ```bash
24
+ ruby examples/widget_style_colors/app.rb
25
+ ```
26
+
27
+ ## Learning Outcomes
28
+
29
+ Use this example if you need to...
30
+ - Create meaningful heatmaps.
31
+ - Generate color palettes dynamically.
32
+ - Test your terminal's color support capabilities.
33
+
34
+ ![Demo](/doc/images/widget_style_colors.png)
@@ -0,0 +1,48 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
4
+ -->
5
+
6
+ # Table Widget Example
7
+
8
+ Demonstrates advanced options for the `Table` widget, including selection, row-level highlighting, and column-level highlighting.
9
+
10
+ Data grids are complex. Users expect to navigate them with keys, select rows, and clearly see which cell is active. The `Table` widget provides these features out of the box efficiently.
11
+
12
+ ## Features Demonstrated
13
+
14
+ - **Selection State**: Managing `selected_row` and `selected_column` to track user focus.
15
+ - **Complex Highlighting**:
16
+ - **Row**: Highlight the entire active row.
17
+ - **Highlight Symbol:** Adding a visual indicator (like `> `) to the selected row.
18
+ - **Spacing:** Adjusting `column_spacing` and `highlight_spacing` to control layout density.
19
+ - **Flex Layout:** Switching between different column distribution modes (`legacy`, `start`, `space_between`, etc.).
20
+ - **Offset Control:** Manually controlling the scroll position using `offset`.
21
+
22
+ ## Hotkeys
23
+
24
+ - **Arrows (↑/↓)**: Navigate Rows (`selected_row`)
25
+ - **Arrows (←/→)**: Navigate Columns (`selected_column`)
26
+ - **x**: Toggle Row Selection (`selected_row` = nil)
27
+ - **s**: Cycle Table Style (`style`)
28
+ - **p**: Cycle Spacing (`highlight_spacing`)
29
+ - **c**: Toggle Column Highlight (`column_highlight_style`)
30
+ - **z**: Toggle Cell Highlight (`cell_highlight_style`)
31
+ - **o**: Cycle Offset Mode (`offset`)
32
+ - **f**: Cycle Flex Mode (`flex`)
33
+ - **q**: Quit
34
+
35
+ ## Usage
36
+
37
+ ```bash
38
+ ruby examples/widget_table_demo/app.rb
39
+ ```
40
+
41
+ ## Learning Outcomes
42
+
43
+ Use this example if you need to...
44
+ - Build a file explorer or process list.
45
+ - Create a data-heavy dashboard.
46
+ - Handle conflicting style requirements (e.g., "Highlight this row, but make this error cell red").
47
+
48
+ ![Demo](/doc/images/widget_table_demo.png)
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env ruby
2
1
  # frozen_string_literal: true
3
2
 
4
3
  # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
@@ -19,7 +18,7 @@ PROCESSES = [
19
18
  { pid: 6789, name: "node", cpu: 18.9 },
20
19
  ].freeze
21
20
 
22
- class AppTableSelect
21
+ class WidgetTableDemo
23
22
  attr_reader :selected_index, :selected_col, :current_style_index, :column_spacing, :highlight_spacing, :column_highlight_style, :cell_highlight_style
24
23
 
25
24
  HIGHLIGHT_SPACINGS = [
@@ -28,6 +27,22 @@ class AppTableSelect
28
27
  { name: "Never", spacing: :never },
29
28
  ].freeze
30
29
 
30
+ OFFSET_MODES = [
31
+ { name: "Auto (No Offset)", offset: nil, allow_selection: true },
32
+ { name: "Offset Only (row 3)", offset: 3, allow_selection: false },
33
+ { name: "Selection + Offset (Conflict)", offset: 0, allow_selection: true },
34
+ ].freeze
35
+
36
+ FLEX_MODES = [
37
+ { name: "Legacy (Default)", flex: :legacy },
38
+ { name: "Start", flex: :start },
39
+ { name: "Center", flex: :center },
40
+ { name: "End", flex: :end },
41
+ { name: "Space Between", flex: :space_between },
42
+ { name: "Space Around", flex: :space_around },
43
+ { name: "Space Evenly", flex: :space_evenly },
44
+ ].freeze
45
+
31
46
  def initialize
32
47
  @selected_index = 1
33
48
  @selected_col = 1
@@ -36,6 +51,8 @@ class AppTableSelect
36
51
  @highlight_spacing_index = 0
37
52
  @show_column_highlight = true
38
53
  @show_cell_highlight = true
54
+ @offset_mode_index = 0
55
+ @flex_mode_index = 0
39
56
  end
40
57
 
41
58
  def run
@@ -80,15 +97,23 @@ class AppTableSelect
80
97
 
81
98
  current_style_entry = @styles[@current_style_index]
82
99
  current_spacing_entry = HIGHLIGHT_SPACINGS[@highlight_spacing_index]
83
- selection_label = @selected_index.nil? ? "none" : @selected_index.to_s
100
+ offset_mode_entry = OFFSET_MODES[@offset_mode_index]
101
+ flex_mode_entry = FLEX_MODES[@flex_mode_index]
102
+
103
+ # Determine selection/offset based on mode
104
+ effective_selection = offset_mode_entry[:allow_selection] ? @selected_index : nil
105
+ effective_offset = offset_mode_entry[:offset]
106
+ selection_label = effective_selection.nil? ? "none" : effective_selection.to_s
107
+ offset_label = effective_offset.nil? ? "auto" : effective_offset.to_s
84
108
 
85
109
  # Main table
86
110
  table = @tui.table(
87
111
  header: ["PID", "Name", "CPU"],
88
112
  rows:,
89
113
  widths:,
90
- selected_row: @selected_index,
114
+ selected_row: effective_selection,
91
115
  selected_column: @selected_col,
116
+ offset: effective_offset,
92
117
  highlight_style:,
93
118
  highlight_symbol: "> ",
94
119
  highlight_spacing: current_spacing_entry[:spacing],
@@ -96,8 +121,9 @@ class AppTableSelect
96
121
  cell_highlight_style: @show_cell_highlight ? @cell_highlight_style : nil,
97
122
  style: current_style_entry[:style],
98
123
  column_spacing: @column_spacing,
124
+ flex: flex_mode_entry[:flex],
99
125
  block: @tui.block(
100
- title: "Processes",
126
+ title: "Processes | Sel: #{selection_label} | Offset: #{offset_label} | Flex: #{flex_mode_entry[:name]}",
101
127
  borders: :all
102
128
  ),
103
129
  footer: ["Total: #{PROCESSES.length}", "Total CPU: #{PROCESSES.sum { |p| p[:cpu] }}%", ""]
@@ -130,11 +156,19 @@ class AppTableSelect
130
156
  ]),
131
157
  # Line 3: More Controls
132
158
  @tui.text_line(spans: [
159
+ @tui.text_span(content: "+/-", style: @hotkey_style),
133
160
  @tui.text_span(content: ": Col Space (#{@column_spacing}) "),
134
161
  @tui.text_span(content: "c", style: @hotkey_style),
135
162
  @tui.text_span(content: ": Col Highlight (#{@show_column_highlight ? 'On' : 'Off'}) "),
163
+ @tui.text_span(content: "f", style: @hotkey_style),
164
+ @tui.text_span(content: ": Flex Mode (#{flex_mode_entry[:name]})"),
165
+ ]),
166
+ # Line 4: Offset Mode
167
+ @tui.text_line(spans: [
136
168
  @tui.text_span(content: "z", style: @hotkey_style),
137
- @tui.text_span(content: ": Cell Highlight (#{@show_cell_highlight ? 'On' : 'Off'})"),
169
+ @tui.text_span(content: ": Cell Highlight (#{@show_cell_highlight ? 'On' : 'Off'}) "),
170
+ @tui.text_span(content: "o", style: @hotkey_style),
171
+ @tui.text_span(content: ": Offset Mode (#{offset_mode_entry[:name]})"),
138
172
  ]),
139
173
  ]
140
174
  ),
@@ -147,7 +181,7 @@ class AppTableSelect
147
181
  direction: :vertical,
148
182
  constraints: [
149
183
  @tui.constraint_fill(1),
150
- @tui.constraint_length(5),
184
+ @tui.constraint_length(6),
151
185
  ]
152
186
  )
153
187
 
@@ -190,6 +224,10 @@ class AppTableSelect
190
224
  @show_column_highlight = !@show_column_highlight
191
225
  in type: :key, code: "z"
192
226
  @show_cell_highlight = !@show_cell_highlight
227
+ in type: :key, code: "o"
228
+ @offset_mode_index = (@offset_mode_index + 1) % OFFSET_MODES.length
229
+ in type: :key, code: "f"
230
+ @flex_mode_index = (@flex_mode_index + 1) % FLEX_MODES.length
193
231
  else
194
232
  nil
195
233
  end
@@ -197,5 +235,5 @@ class AppTableSelect
197
235
  end
198
236
 
199
237
  if __FILE__ == $0
200
- AppTableSelect.new.run
238
+ WidgetTableDemo.new.run
201
239
  end
@@ -0,0 +1,41 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
4
+ -->
5
+
6
+ # Tabs Widget Example
7
+
8
+ Demonstrates view segregation with interactive navigation.
9
+
10
+ Screen real estate is limited. You cannot show everything at once. Tabs segregate content into specialized views (modes), allowing users to switch contexts easily.
11
+
12
+ ## Features Demonstrated
13
+
14
+ - **Condition Rendering**: Changing the *content* of the screen based on the selected tab (Revenue vs Traffic vs Errors).
15
+ - **Styling**: Configurable highlight styles, dividers, and padding.
16
+ - **Interaction**: Keyboard navigation to cycle through tabs.
17
+
18
+ ## Hotkeys
19
+
20
+ - **Left/Right (←/→)**: Select Tab (`selected_index`)
21
+ - **d**: Cycle Divider Character (`divider`)
22
+ - **s**: Cycle Highlight Style (`highlight_style`)
23
+ - **b**: Cycle Base Style (`style`)
24
+ - **h/l**: Adjust Left Padding (`padding_left`)
25
+ - **j/k**: Adjust Right Padding (`padding_right`)
26
+ - **q**: Quit
27
+
28
+ ## Usage
29
+
30
+ ```bash
31
+ ruby examples/widget_tabs_demo/app.rb
32
+ ```
33
+
34
+ ## Learning Outcomes
35
+
36
+ Use this example if you need to...
37
+ - Build a multi-pane dashboard.
38
+ - Create a "Settings" screen with different categories.
39
+ - Implement a "wizard" interface with steps.
40
+
41
+ ![Demo](/doc/images/widget_tabs_demo.png)
@@ -5,6 +5,7 @@
5
5
 
6
6
  $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
7
7
  require "ratatui_ruby"
8
+ require "faker"
8
9
 
9
10
  # Demonstrates view segregation with interactive tab navigation.
10
11
  #
@@ -35,6 +36,9 @@ class WidgetTabsDemo
35
36
  @padding_right = 0
36
37
  @width_constraint_index = 0
37
38
  @hotkey_style = nil
39
+
40
+ # Generate the content once, not on every frame
41
+ @tab_text = 4.times.map { |it| Faker::Lorem.paragraph(sentence_count: 10 + it) }
38
42
  end
39
43
 
40
44
  def run
@@ -77,11 +81,12 @@ class WidgetTabsDemo
77
81
  )
78
82
 
79
83
  # Center the tabs vertically in the main area
80
- tabs_area, = @tui.layout_split(
84
+ tabs_area, content_area = @tui.layout_split(
81
85
  main_area,
82
86
  direction: :vertical,
83
87
  constraints: [
84
88
  @tui.constraint_length(3),
89
+ @tui.constraint_fill(1),
85
90
  ]
86
91
  )
87
92
 
@@ -96,6 +101,7 @@ class WidgetTabsDemo
96
101
  padding_right: @padding_right
97
102
  )
98
103
  frame.render_widget(tabs, tabs_area)
104
+ frame.render_widget(tab_contents, content_area)
99
105
 
100
106
  render_controls(frame, controls_area, tabs.width)
101
107
  end
@@ -162,6 +168,14 @@ class WidgetTabsDemo
162
168
  # Ignore other events
163
169
  end
164
170
  end
171
+
172
+ private def tab_contents
173
+ @tui.paragraph(
174
+ text: @tab_text[@selected_tab],
175
+ wrap: true,
176
+ block: @tui.block(borders: [:all], title: @tabs[@selected_tab])
177
+ )
178
+ end
165
179
  end
166
180
 
167
181
  WidgetTabsDemo.new.run if __FILE__ == $0
@@ -0,0 +1,35 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
4
+ -->
5
+
6
+ # Text Width Calculator
7
+
8
+ Demonstrates string width calculation in a terminal environment.
9
+
10
+ Not all characters are created equal. In a TUI, "Width" means cell count, not string length. Emoji (`👍`) take 2 cells. Chinese characters (`你`) take 2 cells. The `tui.text_width` helper tells you the visual width of a string.
11
+
12
+ ## Features Demonstrated
13
+
14
+ - **Unicode Width**: Rendering ASCII (1 cell), CJK (2 cells), and Emoji (2 cells).
15
+ - **Calculation**: Comparing `string.length` vs `tui.text_width(string)`.
16
+
17
+ ## Hotkeys
18
+
19
+ - **Up/Down (↑/↓)**: Cycle Text Sample
20
+ - **q**: Quit
21
+
22
+ ## Usage
23
+
24
+ ```bash
25
+ ruby examples/widget_text_width/app.rb
26
+ ```
27
+
28
+ ## Learning Outcomes
29
+
30
+ Use this example if you need to...
31
+ - Align text correctly in columns.
32
+ - Truncate strings that are too long for a widget.
33
+ - Build your own custom layout engine.
34
+
35
+ ![Demo](/doc/images/widget_text_width.png)
@@ -0,0 +1,106 @@
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
+ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
7
+ require "ratatui_ruby"
8
+
9
+ class WidgetTextWidth
10
+ def initialize
11
+ @text_samples = [
12
+ { label: "ASCII", text: "Hello, World!", desc: "Simple English text" },
13
+ { label: "CJK", text: "你好世界", desc: "Chinese (full-width characters)" },
14
+ { label: "Emoji", text: "Hello 👍 World 🌍", desc: "Mixed text with emoji (2 cells each)" },
15
+ { label: "Mixed", text: "Hi 你好 👍", desc: "ASCII + CJK + emoji" },
16
+ { label: "Empty", text: "", desc: "Empty string" },
17
+ ]
18
+ @selected_index = 0
19
+ end
20
+
21
+ def run
22
+ RatatuiRuby.run do |tui|
23
+ @tui = tui
24
+ loop do
25
+ render
26
+ break if handle_input == :quit
27
+ end
28
+ end
29
+ end
30
+
31
+ private def render
32
+ @tui.draw do |frame|
33
+ # Layout: main content above, controls below
34
+ areas = @tui.layout_split(
35
+ frame.area,
36
+ direction: :vertical,
37
+ constraints: [@tui.constraint_fill(1), @tui.constraint_length(7)]
38
+ )
39
+
40
+ # Main content area with sample text
41
+ render_content(frame, areas[0])
42
+
43
+ # Controls footer
44
+ render_controls(frame, areas[1])
45
+ end
46
+ end
47
+
48
+ private def render_content(frame, area)
49
+ sample = @text_samples[@selected_index]
50
+ measured_width = @tui.text_width(sample[:text])
51
+
52
+ # Build content text with newlines
53
+ content = []
54
+ content << "Sample: #{sample[:text]}"
55
+ content << ""
56
+ content << "Display Width: #{measured_width} cells"
57
+ content << "Character Count: #{sample[:text].length}"
58
+ content << ""
59
+ content << sample[:desc]
60
+ text = content.join("\n")
61
+
62
+ widget = @tui.paragraph(
63
+ text:,
64
+ block: @tui.block(
65
+ title: "Text Width Calculator",
66
+ borders: [:all],
67
+ border_color: "cyan"
68
+ ),
69
+ alignment: :left
70
+ )
71
+
72
+ frame.render_widget(widget, area)
73
+ end
74
+
75
+ private def render_controls(frame, area)
76
+ info = "Sample #{@selected_index + 1}/#{@text_samples.length}: #{@text_samples[@selected_index][:label]}"
77
+ controls = "↑/↓ Select q Quit"
78
+ text = "#{info}\n#{controls}"
79
+
80
+ widget = @tui.paragraph(
81
+ text:,
82
+ block: @tui.block(borders: [:top], border_color: "gray"),
83
+ alignment: :center
84
+ )
85
+
86
+ frame.render_widget(widget, area)
87
+ end
88
+
89
+ private def handle_input
90
+ event = @tui.poll_event
91
+ case event
92
+ in { type: :key, code: "q" }
93
+ :quit
94
+ in { type: :key, code: "up" }
95
+ @selected_index = (@selected_index - 1) % @text_samples.length
96
+ nil
97
+ in { type: :key, code: "down" }
98
+ @selected_index = (@selected_index + 1) % @text_samples.length
99
+ nil
100
+ else
101
+ nil
102
+ end
103
+ end
104
+ end
105
+
106
+ WidgetTextWidth.new.run if __FILE__ == $PROGRAM_NAME
data/exe/.gitkeep ADDED
File without changes
@@ -956,7 +956,7 @@ dependencies = [
956
956
  "thiserror 2.0.17",
957
957
  "unicode-segmentation",
958
958
  "unicode-truncate",
959
- "unicode-width",
959
+ "unicode-width 0.2.0",
960
960
  ]
961
961
 
962
962
  [[package]]
@@ -1007,18 +1007,19 @@ dependencies = [
1007
1007
  "strum",
1008
1008
  "time",
1009
1009
  "unicode-segmentation",
1010
- "unicode-width",
1010
+ "unicode-width 0.2.0",
1011
1011
  ]
1012
1012
 
1013
1013
  [[package]]
1014
1014
  name = "ratatui_ruby"
1015
- version = "0.5.0"
1015
+ version = "0.6.0"
1016
1016
  dependencies = [
1017
1017
  "bumpalo",
1018
1018
  "lazy_static",
1019
1019
  "magnus",
1020
1020
  "ratatui",
1021
1021
  "time",
1022
+ "unicode-width 0.1.14",
1022
1023
  ]
1023
1024
 
1024
1025
  [[package]]
@@ -1464,9 +1465,15 @@ checksum = "8fbf03860ff438702f3910ca5f28f8dac63c1c11e7efb5012b8b175493606330"
1464
1465
  dependencies = [
1465
1466
  "itertools 0.13.0",
1466
1467
  "unicode-segmentation",
1467
- "unicode-width",
1468
+ "unicode-width 0.2.0",
1468
1469
  ]
1469
1470
 
1471
+ [[package]]
1472
+ name = "unicode-width"
1473
+ version = "0.1.14"
1474
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1475
+ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
1476
+
1470
1477
  [[package]]
1471
1478
  name = "unicode-width"
1472
1479
  version = "0.2.0"
@@ -3,7 +3,7 @@
3
3
 
4
4
  [package]
5
5
  name = "ratatui_ruby"
6
- version = "0.5.0"
6
+ version = "0.6.0"
7
7
  edition = "2021"
8
8
 
9
9
  [lib]
@@ -12,6 +12,7 @@ crate-type = ["cdylib", "staticlib"]
12
12
  [dependencies]
13
13
  magnus = "0.8.2"
14
14
  ratatui = { version = "0.30", features = ["widget-calendar", "layout-cache", "unstable-rendered-line-info"] }
15
+ unicode-width = "0.1"
15
16
 
16
17
  bumpalo = "3.16"
17
18
  lazy_static = "1.4"