ratatui_ruby 0.5.0 → 0.7.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 (311) 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 +10 -4
  7. data/CHANGELOG.md +79 -7
  8. data/README.md +37 -5
  9. data/REUSE.toml +2 -7
  10. data/doc/application_architecture.md +96 -22
  11. data/doc/application_testing.md +76 -30
  12. data/doc/contributors/architectural_overhaul/chat_conversations.md +4952 -0
  13. data/doc/contributors/architectural_overhaul/implementation_plan.md +60 -0
  14. data/doc/contributors/architectural_overhaul/task.md +37 -0
  15. data/doc/contributors/design/ruby_frontend.md +288 -56
  16. data/doc/contributors/design/rust_backend.md +349 -54
  17. data/doc/contributors/developing_examples.md +134 -49
  18. data/doc/contributors/index.md +7 -5
  19. data/doc/contributors/v1.0.0_blockers.md +1729 -0
  20. data/doc/event_handling.md +11 -3
  21. data/doc/images/app_all_events.png +0 -0
  22. data/doc/images/app_color_picker.png +0 -0
  23. data/doc/images/app_login_form.png +0 -0
  24. data/doc/images/app_stateful_interaction.png +0 -0
  25. data/doc/images/verify_quickstart_dsl.png +0 -0
  26. data/doc/images/verify_quickstart_layout.png +0 -0
  27. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  28. data/doc/images/verify_readme_usage.png +0 -0
  29. data/doc/images/widget_barchart_demo.png +0 -0
  30. data/doc/images/widget_block_demo.png +0 -0
  31. data/doc/images/widget_canvas_demo.png +0 -0
  32. data/doc/images/widget_cell_demo.png +0 -0
  33. data/doc/images/widget_center_demo.png +0 -0
  34. data/doc/images/widget_chart_demo.png +0 -0
  35. data/doc/images/widget_list_demo.png +0 -0
  36. data/doc/images/widget_overlay_demo.png +0 -0
  37. data/doc/images/widget_render.png +0 -0
  38. data/doc/images/widget_rich_text.png +0 -0
  39. data/doc/images/widget_scroll_text.png +0 -0
  40. data/doc/images/widget_sparkline_demo.png +0 -0
  41. data/doc/images/widget_table_demo.png +0 -0
  42. data/doc/images/widget_tabs_demo.png +0 -0
  43. data/doc/images/widget_text_width.png +0 -0
  44. data/doc/index.md +11 -6
  45. data/doc/interactive_design.md +2 -2
  46. data/doc/quickstart.md +127 -165
  47. data/doc/terminal_limitations.md +92 -0
  48. data/doc/v0.7.0_migration.md +236 -0
  49. data/doc/why.md +93 -0
  50. data/examples/app_all_events/README.md +47 -27
  51. data/examples/app_all_events/app.rb +38 -35
  52. data/examples/app_all_events/model/app_model.rb +157 -0
  53. data/examples/app_all_events/model/event_entry.rb +17 -0
  54. data/examples/app_all_events/model/msg.rb +37 -0
  55. data/examples/app_all_events/update.rb +73 -0
  56. data/examples/app_all_events/view/app_view.rb +9 -9
  57. data/examples/app_all_events/view/controls_view.rb +9 -7
  58. data/examples/app_all_events/view/counts_view.rb +13 -9
  59. data/examples/app_all_events/view/live_view.rb +9 -8
  60. data/examples/app_all_events/view/log_view.rb +11 -16
  61. data/examples/app_color_picker/README.md +84 -42
  62. data/examples/app_color_picker/app.rb +24 -62
  63. data/examples/app_color_picker/controls.rb +90 -0
  64. data/examples/app_color_picker/copy_dialog.rb +45 -49
  65. data/examples/app_color_picker/export_pane.rb +126 -0
  66. data/examples/app_color_picker/input.rb +99 -67
  67. data/examples/app_color_picker/main_container.rb +178 -0
  68. data/examples/app_color_picker/palette.rb +55 -26
  69. data/examples/app_login_form/README.md +49 -0
  70. data/examples/app_login_form/app.rb +2 -3
  71. data/examples/app_stateful_interaction/README.md +33 -0
  72. data/examples/app_stateful_interaction/app.rb +272 -0
  73. data/examples/timeout_demo.rb +43 -0
  74. data/examples/verify_quickstart_dsl/README.md +49 -0
  75. data/examples/verify_quickstart_dsl/app.rb +2 -0
  76. data/examples/verify_quickstart_layout/README.md +71 -0
  77. data/examples/verify_quickstart_layout/app.rb +2 -0
  78. data/examples/verify_quickstart_lifecycle/README.md +56 -0
  79. data/examples/verify_quickstart_lifecycle/app.rb +10 -4
  80. data/examples/verify_readme_usage/README.md +43 -0
  81. data/examples/verify_readme_usage/app.rb +8 -2
  82. data/examples/widget_barchart_demo/README.md +50 -0
  83. data/examples/widget_barchart_demo/app.rb +5 -5
  84. data/examples/widget_block_demo/README.md +36 -0
  85. data/examples/widget_block_demo/app.rb +256 -0
  86. data/examples/widget_box_demo/README.md +45 -0
  87. data/examples/widget_calendar_demo/README.md +39 -0
  88. data/examples/widget_calendar_demo/app.rb +5 -1
  89. data/examples/widget_canvas_demo/README.md +27 -0
  90. data/examples/widget_canvas_demo/app.rb +123 -0
  91. data/examples/widget_cell_demo/README.md +36 -0
  92. data/examples/widget_cell_demo/app.rb +31 -24
  93. data/examples/widget_center_demo/README.md +29 -0
  94. data/examples/widget_center_demo/app.rb +116 -0
  95. data/examples/widget_chart_demo/README.md +41 -0
  96. data/examples/widget_chart_demo/app.rb +7 -2
  97. data/examples/widget_gauge_demo/README.md +41 -0
  98. data/examples/widget_layout_split/README.md +44 -0
  99. data/examples/widget_line_gauge_demo/README.md +41 -0
  100. data/examples/widget_list_demo/README.md +49 -0
  101. data/examples/widget_list_demo/app.rb +91 -107
  102. data/examples/widget_map_demo/README.md +39 -0
  103. data/examples/{app_map_demo → widget_map_demo}/app.rb +4 -4
  104. data/examples/widget_overlay_demo/README.md +36 -0
  105. data/examples/widget_overlay_demo/app.rb +248 -0
  106. data/examples/widget_popup_demo/README.md +36 -0
  107. data/examples/widget_ratatui_logo_demo/README.md +34 -0
  108. data/examples/widget_ratatui_logo_demo/app.rb +1 -1
  109. data/examples/widget_ratatui_mascot_demo/README.md +34 -0
  110. data/examples/widget_rect/README.md +38 -0
  111. data/examples/widget_render/README.md +37 -0
  112. data/examples/widget_render/app.rb +3 -3
  113. data/examples/widget_rich_text/README.md +35 -0
  114. data/examples/widget_rich_text/app.rb +62 -33
  115. data/examples/widget_scroll_text/README.md +37 -0
  116. data/examples/widget_scroll_text/app.rb +0 -1
  117. data/examples/widget_scrollbar_demo/README.md +37 -0
  118. data/examples/widget_sparkline_demo/README.md +42 -0
  119. data/examples/widget_sparkline_demo/app.rb +4 -3
  120. data/examples/widget_style_colors/README.md +34 -0
  121. data/examples/widget_table_demo/README.md +48 -0
  122. data/examples/{app_table_select → widget_table_demo}/app.rb +65 -12
  123. data/examples/widget_tabs_demo/README.md +41 -0
  124. data/examples/widget_tabs_demo/app.rb +15 -1
  125. data/examples/widget_text_width/README.md +35 -0
  126. data/examples/widget_text_width/app.rb +113 -0
  127. data/exe/.gitkeep +0 -0
  128. data/ext/ratatui_ruby/Cargo.lock +11 -4
  129. data/ext/ratatui_ruby/Cargo.toml +2 -1
  130. data/ext/ratatui_ruby/src/events.rs +238 -26
  131. data/ext/ratatui_ruby/src/frame.rs +116 -3
  132. data/ext/ratatui_ruby/src/lib.rs +37 -6
  133. data/ext/ratatui_ruby/src/rendering.rs +22 -21
  134. data/ext/ratatui_ruby/src/string_width.rs +101 -0
  135. data/ext/ratatui_ruby/src/terminal.rs +39 -15
  136. data/ext/ratatui_ruby/src/text.rs +13 -4
  137. data/ext/ratatui_ruby/src/widgets/barchart.rs +24 -6
  138. data/ext/ratatui_ruby/src/widgets/canvas.rs +5 -5
  139. data/ext/ratatui_ruby/src/widgets/gauge.rs +9 -2
  140. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +9 -2
  141. data/ext/ratatui_ruby/src/widgets/list.rs +179 -3
  142. data/ext/ratatui_ruby/src/widgets/list_state.rs +137 -0
  143. data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
  144. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +93 -1
  145. data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +169 -0
  146. data/ext/ratatui_ruby/src/widgets/table.rs +191 -34
  147. data/ext/ratatui_ruby/src/widgets/table_state.rs +121 -0
  148. data/lib/ratatui_ruby/buffer/cell.rb +168 -0
  149. data/lib/ratatui_ruby/buffer.rb +15 -0
  150. data/lib/ratatui_ruby/cell.rb +4 -4
  151. data/lib/ratatui_ruby/event/key/character.rb +35 -0
  152. data/lib/ratatui_ruby/event/key/media.rb +44 -0
  153. data/lib/ratatui_ruby/event/key/modifier.rb +95 -0
  154. data/lib/ratatui_ruby/event/key/navigation.rb +55 -0
  155. data/lib/ratatui_ruby/event/key/system.rb +45 -0
  156. data/lib/ratatui_ruby/event/key.rb +111 -51
  157. data/lib/ratatui_ruby/event/mouse.rb +3 -3
  158. data/lib/ratatui_ruby/event/paste.rb +1 -1
  159. data/lib/ratatui_ruby/frame.rb +100 -4
  160. data/lib/ratatui_ruby/layout/constraint.rb +95 -0
  161. data/lib/ratatui_ruby/layout/layout.rb +106 -0
  162. data/lib/ratatui_ruby/layout/rect.rb +118 -0
  163. data/lib/ratatui_ruby/layout.rb +19 -0
  164. data/lib/ratatui_ruby/list_state.rb +88 -0
  165. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +2 -2
  166. data/lib/ratatui_ruby/schema/cursor.rb +5 -0
  167. data/lib/ratatui_ruby/schema/gauge.rb +3 -1
  168. data/lib/ratatui_ruby/schema/layout.rb +1 -1
  169. data/lib/ratatui_ruby/schema/line_gauge.rb +2 -2
  170. data/lib/ratatui_ruby/schema/list.rb +25 -4
  171. data/lib/ratatui_ruby/schema/list_item.rb +41 -0
  172. data/lib/ratatui_ruby/schema/rect.rb +43 -0
  173. data/lib/ratatui_ruby/schema/row.rb +66 -0
  174. data/lib/ratatui_ruby/schema/style.rb +24 -4
  175. data/lib/ratatui_ruby/schema/table.rb +29 -11
  176. data/lib/ratatui_ruby/schema/text.rb +96 -3
  177. data/lib/ratatui_ruby/scrollbar_state.rb +112 -0
  178. data/lib/ratatui_ruby/style/style.rb +81 -0
  179. data/lib/ratatui_ruby/style.rb +15 -0
  180. data/lib/ratatui_ruby/table_state.rb +90 -0
  181. data/lib/ratatui_ruby/test_helper/event_injection.rb +169 -0
  182. data/lib/ratatui_ruby/test_helper/snapshot.rb +414 -0
  183. data/lib/ratatui_ruby/test_helper/style_assertions.rb +351 -0
  184. data/lib/ratatui_ruby/test_helper/terminal.rb +127 -0
  185. data/lib/ratatui_ruby/test_helper/test_doubles.rb +68 -0
  186. data/lib/ratatui_ruby/test_helper.rb +65 -358
  187. data/lib/ratatui_ruby/tui/buffer_factories.rb +20 -0
  188. data/lib/ratatui_ruby/tui/canvas_factories.rb +44 -0
  189. data/lib/ratatui_ruby/tui/core.rb +38 -0
  190. data/lib/ratatui_ruby/tui/layout_factories.rb +74 -0
  191. data/lib/ratatui_ruby/tui/state_factories.rb +33 -0
  192. data/lib/ratatui_ruby/tui/style_factories.rb +20 -0
  193. data/lib/ratatui_ruby/tui/text_factories.rb +44 -0
  194. data/lib/ratatui_ruby/tui/widget_factories.rb +195 -0
  195. data/lib/ratatui_ruby/tui.rb +75 -0
  196. data/lib/ratatui_ruby/version.rb +1 -1
  197. data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +47 -0
  198. data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +25 -0
  199. data/lib/ratatui_ruby/widgets/bar_chart.rb +239 -0
  200. data/lib/ratatui_ruby/widgets/block.rb +192 -0
  201. data/lib/ratatui_ruby/widgets/calendar.rb +84 -0
  202. data/lib/ratatui_ruby/widgets/canvas.rb +231 -0
  203. data/lib/ratatui_ruby/widgets/cell.rb +47 -0
  204. data/lib/ratatui_ruby/widgets/center.rb +59 -0
  205. data/lib/ratatui_ruby/widgets/chart.rb +185 -0
  206. data/lib/ratatui_ruby/widgets/clear.rb +54 -0
  207. data/lib/ratatui_ruby/widgets/cursor.rb +42 -0
  208. data/lib/ratatui_ruby/widgets/gauge.rb +72 -0
  209. data/lib/ratatui_ruby/widgets/line_gauge.rb +80 -0
  210. data/lib/ratatui_ruby/widgets/list.rb +127 -0
  211. data/lib/ratatui_ruby/widgets/list_item.rb +43 -0
  212. data/lib/ratatui_ruby/widgets/overlay.rb +43 -0
  213. data/lib/ratatui_ruby/widgets/paragraph.rb +99 -0
  214. data/lib/ratatui_ruby/widgets/ratatui_logo.rb +31 -0
  215. data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +36 -0
  216. data/lib/ratatui_ruby/widgets/row.rb +68 -0
  217. data/lib/ratatui_ruby/widgets/scrollbar.rb +143 -0
  218. data/lib/ratatui_ruby/widgets/shape/label.rb +68 -0
  219. data/lib/ratatui_ruby/widgets/sparkline.rb +134 -0
  220. data/lib/ratatui_ruby/widgets/table.rb +141 -0
  221. data/lib/ratatui_ruby/widgets/tabs.rb +85 -0
  222. data/lib/ratatui_ruby/widgets.rb +40 -0
  223. data/lib/ratatui_ruby.rb +64 -57
  224. data/sig/examples/app_all_events/view.rbs +1 -1
  225. data/sig/examples/app_all_events/view_state.rbs +1 -1
  226. data/sig/examples/app_stateful_interaction/app.rbs +33 -0
  227. data/sig/examples/widget_block_demo/app.rbs +32 -0
  228. data/sig/examples/{app_map_demo → widget_map_demo}/app.rbs +2 -2
  229. data/sig/examples/{app_table_select → widget_table_demo}/app.rbs +2 -2
  230. data/sig/examples/{widget_table_flex → widget_text_width}/app.rbs +2 -3
  231. data/sig/ratatui_ruby/event.rbs +11 -1
  232. data/sig/ratatui_ruby/frame.rbs +2 -0
  233. data/sig/ratatui_ruby/list_state.rbs +13 -0
  234. data/sig/ratatui_ruby/ratatui_ruby.rbs +2 -2
  235. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +3 -3
  236. data/sig/ratatui_ruby/schema/gauge.rbs +2 -2
  237. data/sig/ratatui_ruby/schema/line_gauge.rbs +2 -2
  238. data/sig/ratatui_ruby/schema/list.rbs +4 -2
  239. data/sig/ratatui_ruby/schema/list_item.rbs +10 -0
  240. data/sig/ratatui_ruby/schema/rect.rbs +3 -0
  241. data/sig/ratatui_ruby/schema/row.rbs +22 -0
  242. data/sig/ratatui_ruby/schema/style.rbs +3 -3
  243. data/sig/ratatui_ruby/schema/table.rbs +3 -1
  244. data/sig/ratatui_ruby/schema/text.rbs +9 -6
  245. data/sig/ratatui_ruby/scrollbar_state.rbs +18 -0
  246. data/sig/ratatui_ruby/session.rbs +41 -48
  247. data/sig/ratatui_ruby/table_state.rbs +15 -0
  248. data/sig/ratatui_ruby/test_helper/event_injection.rbs +16 -0
  249. data/sig/ratatui_ruby/test_helper/snapshot.rbs +12 -0
  250. data/sig/ratatui_ruby/test_helper/style_assertions.rbs +64 -0
  251. data/sig/ratatui_ruby/test_helper/terminal.rbs +14 -0
  252. data/sig/ratatui_ruby/test_helper/test_doubles.rbs +22 -0
  253. data/sig/ratatui_ruby/test_helper.rbs +5 -4
  254. data/sig/ratatui_ruby/tui/buffer_factories.rbs +10 -0
  255. data/sig/ratatui_ruby/tui/canvas_factories.rbs +14 -0
  256. data/sig/ratatui_ruby/tui/core.rbs +14 -0
  257. data/sig/ratatui_ruby/tui/layout_factories.rbs +19 -0
  258. data/sig/ratatui_ruby/tui/state_factories.rbs +12 -0
  259. data/sig/ratatui_ruby/tui/style_factories.rbs +10 -0
  260. data/sig/ratatui_ruby/tui/text_factories.rbs +14 -0
  261. data/sig/ratatui_ruby/tui/widget_factories.rbs +39 -0
  262. data/sig/ratatui_ruby/tui.rbs +19 -0
  263. data/tasks/autodoc/examples.rb +79 -0
  264. data/tasks/autodoc.rake +7 -35
  265. data/tasks/bump/changelog.rb +3 -3
  266. data/tasks/bump/links.rb +67 -0
  267. data/tasks/sourcehut.rake +64 -21
  268. data/tasks/terminal_preview/app_screenshot.rb +13 -3
  269. data/tasks/terminal_preview/saved_screenshot.rb +4 -3
  270. metadata +169 -48
  271. data/doc/contributors/dwim_dx.md +0 -366
  272. data/doc/images/app_analytics.png +0 -0
  273. data/doc/images/app_custom_widget.png +0 -0
  274. data/doc/images/app_mouse_events.png +0 -0
  275. data/doc/images/app_table_select.png +0 -0
  276. data/doc/images/widget_block_padding.png +0 -0
  277. data/doc/images/widget_block_titles.png +0 -0
  278. data/doc/images/widget_list_styles.png +0 -0
  279. data/doc/images/widget_table_flex.png +0 -0
  280. data/examples/app_all_events/model/events.rb +0 -180
  281. data/examples/app_all_events/model/highlight.rb +0 -57
  282. data/examples/app_all_events/test/snapshots/after_focus_lost.txt +0 -24
  283. data/examples/app_all_events/test/snapshots/after_focus_regained.txt +0 -24
  284. data/examples/app_all_events/test/snapshots/after_horizontal_resize.txt +0 -24
  285. data/examples/app_all_events/test/snapshots/after_key_a.txt +0 -24
  286. data/examples/app_all_events/test/snapshots/after_key_ctrl_x.txt +0 -24
  287. data/examples/app_all_events/test/snapshots/after_mouse_click.txt +0 -24
  288. data/examples/app_all_events/test/snapshots/after_mouse_drag.txt +0 -24
  289. data/examples/app_all_events/test/snapshots/after_multiple_events.txt +0 -24
  290. data/examples/app_all_events/test/snapshots/after_paste.txt +0 -24
  291. data/examples/app_all_events/test/snapshots/after_resize.txt +0 -24
  292. data/examples/app_all_events/test/snapshots/after_right_click.txt +0 -24
  293. data/examples/app_all_events/test/snapshots/after_vertical_resize.txt +0 -24
  294. data/examples/app_all_events/test/snapshots/initial_state.txt +0 -24
  295. data/examples/app_all_events/view_state.rb +0 -42
  296. data/examples/app_color_picker/scene.rb +0 -201
  297. data/examples/widget_block_padding/app.rb +0 -67
  298. data/examples/widget_block_titles/app.rb +0 -69
  299. data/examples/widget_list_styles/app.rb +0 -141
  300. data/examples/widget_table_flex/app.rb +0 -95
  301. data/lib/ratatui_ruby/session/autodoc.rb +0 -417
  302. data/lib/ratatui_ruby/session.rb +0 -163
  303. data/sig/examples/widget_block_padding/app.rbs +0 -11
  304. data/sig/examples/widget_block_titles/app.rbs +0 -11
  305. data/sig/examples/widget_list_styles/app.rbs +0 -11
  306. data/tasks/autodoc/inventory.rb +0 -61
  307. data/tasks/autodoc/notice.rb +0 -26
  308. data/tasks/autodoc/rbs.rb +0 -38
  309. data/tasks/autodoc/rdoc.rb +0 -45
  310. data/tasks/bump/comparison_links.rb +0 -41
  311. /data/doc/images/{app_map_demo.png → widget_map_demo.png} +0 -0
@@ -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
@@ -65,8 +82,23 @@ class AppTableSelect
65
82
  end
66
83
 
67
84
  private def render(frame)
68
- # Create table rows from process data
69
- rows = PROCESSES.map { |p| [p[:pid].to_s, p[:name], "#{p[:cpu]}%"] }
85
+ # v0.7.0: Create table rows using table_row and table_cell for per-cell styling
86
+ rows = PROCESSES.map do |p|
87
+ cpu_style = case p[:cpu]
88
+ when 0...10 then @tui.style(fg: :green)
89
+ when 10...30 then @tui.style(fg: :yellow)
90
+ else @tui.style(fg: :red, modifiers: [:bold])
91
+ end
92
+ @tui.table_row(
93
+ cells: [
94
+ p[:pid].to_s,
95
+ p[:name],
96
+ @tui.table_cell(content: "#{p[:cpu]}%", style: cpu_style),
97
+ ],
98
+ # Apply alternating row backgrounds for readability
99
+ style: p[:pid].even? ? @tui.style(bg: :dark_gray) : nil
100
+ )
101
+ end
70
102
 
71
103
  # Define column widths
72
104
  widths = [
@@ -76,28 +108,37 @@ class AppTableSelect
76
108
  ]
77
109
 
78
110
  # Create highlight style (yellow text)
79
- highlight_style = @tui.style(fg: :yellow)
111
+ row_highlight_style = @tui.style(fg: :yellow)
80
112
 
81
113
  current_style_entry = @styles[@current_style_index]
82
114
  current_spacing_entry = HIGHLIGHT_SPACINGS[@highlight_spacing_index]
83
- selection_label = @selected_index.nil? ? "none" : @selected_index.to_s
115
+ offset_mode_entry = OFFSET_MODES[@offset_mode_index]
116
+ flex_mode_entry = FLEX_MODES[@flex_mode_index]
117
+
118
+ # Determine selection/offset based on mode
119
+ effective_selection = offset_mode_entry[:allow_selection] ? @selected_index : nil
120
+ effective_offset = offset_mode_entry[:offset]
121
+ selection_label = effective_selection.nil? ? "none" : effective_selection.to_s
122
+ offset_label = effective_offset.nil? ? "auto" : effective_offset.to_s
84
123
 
85
124
  # Main table
86
125
  table = @tui.table(
87
126
  header: ["PID", "Name", "CPU"],
88
127
  rows:,
89
128
  widths:,
90
- selected_row: @selected_index,
129
+ selected_row: effective_selection,
91
130
  selected_column: @selected_col,
92
- highlight_style:,
131
+ offset: effective_offset,
132
+ row_highlight_style:,
93
133
  highlight_symbol: "> ",
94
134
  highlight_spacing: current_spacing_entry[:spacing],
95
135
  column_highlight_style: @show_column_highlight ? @column_highlight_style : nil,
96
136
  cell_highlight_style: @show_cell_highlight ? @cell_highlight_style : nil,
97
137
  style: current_style_entry[:style],
98
138
  column_spacing: @column_spacing,
139
+ flex: flex_mode_entry[:flex],
99
140
  block: @tui.block(
100
- title: "Processes",
141
+ title: "Processes | Sel: #{selection_label} | Offset: #{offset_label} | Flex: #{flex_mode_entry[:name]}",
101
142
  borders: :all
102
143
  ),
103
144
  footer: ["Total: #{PROCESSES.length}", "Total CPU: #{PROCESSES.sum { |p| p[:cpu] }}%", ""]
@@ -130,11 +171,19 @@ class AppTableSelect
130
171
  ]),
131
172
  # Line 3: More Controls
132
173
  @tui.text_line(spans: [
174
+ @tui.text_span(content: "+/-", style: @hotkey_style),
133
175
  @tui.text_span(content: ": Col Space (#{@column_spacing}) "),
134
176
  @tui.text_span(content: "c", style: @hotkey_style),
135
177
  @tui.text_span(content: ": Col Highlight (#{@show_column_highlight ? 'On' : 'Off'}) "),
178
+ @tui.text_span(content: "f", style: @hotkey_style),
179
+ @tui.text_span(content: ": Flex Mode (#{flex_mode_entry[:name]})"),
180
+ ]),
181
+ # Line 4: Offset Mode
182
+ @tui.text_line(spans: [
136
183
  @tui.text_span(content: "z", style: @hotkey_style),
137
- @tui.text_span(content: ": Cell Highlight (#{@show_cell_highlight ? 'On' : 'Off'})"),
184
+ @tui.text_span(content: ": Cell Highlight (#{@show_cell_highlight ? 'On' : 'Off'}) "),
185
+ @tui.text_span(content: "o", style: @hotkey_style),
186
+ @tui.text_span(content: ": Offset Mode (#{offset_mode_entry[:name]})"),
138
187
  ]),
139
188
  ]
140
189
  ),
@@ -147,7 +196,7 @@ class AppTableSelect
147
196
  direction: :vertical,
148
197
  constraints: [
149
198
  @tui.constraint_fill(1),
150
- @tui.constraint_length(5),
199
+ @tui.constraint_length(6),
151
200
  ]
152
201
  )
153
202
 
@@ -190,6 +239,10 @@ class AppTableSelect
190
239
  @show_column_highlight = !@show_column_highlight
191
240
  in type: :key, code: "z"
192
241
  @show_cell_highlight = !@show_cell_highlight
242
+ in type: :key, code: "o"
243
+ @offset_mode_index = (@offset_mode_index + 1) % OFFSET_MODES.length
244
+ in type: :key, code: "f"
245
+ @flex_mode_index = (@flex_mode_index + 1) % FLEX_MODES.length
193
246
  else
194
247
  nil
195
248
  end
@@ -197,5 +250,5 @@ class AppTableSelect
197
250
  end
198
251
 
199
252
  if __FILE__ == $0
200
- AppTableSelect.new.run
253
+ WidgetTableDemo.new.run
201
254
  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
+ [![widget_tabs_demo](../../doc/images/widget_tabs_demo.png)](app.rb)
9
+
10
+ Demonstrates view segregation with interactive navigation.
11
+
12
+ Screen real estate is limited. You cannot show everything at once. Tabs segregate content into specialized views (modes), allowing users to switch contexts easily.
13
+
14
+ ## Features Demonstrated
15
+
16
+ - **Condition Rendering**: Changing the *content* of the screen based on the selected tab (Revenue vs Traffic vs Errors).
17
+ - **Styling**: Configurable highlight styles, dividers, and padding.
18
+ - **Interaction**: Keyboard navigation to cycle through tabs.
19
+
20
+ ## Hotkeys
21
+
22
+ - **Left/Right (←/→)**: Select Tab (`selected_index`)
23
+ - **d**: Cycle Divider Character (`divider`)
24
+ - **s**: Cycle Highlight Style (`highlight_style`)
25
+ - **b**: Cycle Base Style (`style`)
26
+ - **h/l**: Adjust Left Padding (`padding_left`)
27
+ - **j/k**: Adjust Right Padding (`padding_right`)
28
+ - **q**: Quit
29
+
30
+ ## Usage
31
+
32
+ ```bash
33
+ ruby examples/widget_tabs_demo/app.rb
34
+ ```
35
+
36
+ ## Learning Outcomes
37
+
38
+ Use this example if you need to...
39
+ - Build a multi-pane dashboard.
40
+ - Create a "Settings" screen with different categories.
41
+ - Implement a "wizard" interface with steps.
@@ -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
+ [![widget_text_width](../../doc/images/widget_text_width.png)](app.rb)
9
+
10
+ Demonstrates string width calculation in a terminal environment.
11
+
12
+ 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.
13
+
14
+ ## Features Demonstrated
15
+
16
+ - **Unicode Width**: Rendering ASCII (1 cell), CJK (2 cells), and Emoji (2 cells).
17
+ - **Calculation**: Comparing `string.length` vs `tui.text_width(string)`.
18
+
19
+ ## Hotkeys
20
+
21
+ - **Up/Down (↑/↓)**: Cycle Text Sample
22
+ - **q**: Quit
23
+
24
+ ## Usage
25
+
26
+ ```bash
27
+ ruby examples/widget_text_width/app.rb
28
+ ```
29
+
30
+ ## Learning Outcomes
31
+
32
+ Use this example if you need to...
33
+ - Align text correctly in columns.
34
+ - Truncate strings that are too long for a widget.
35
+ - Build your own custom layout engine.
@@ -0,0 +1,113 @@
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
+ # v0.7.0: Text::Line#width instance method for rich text measurement
53
+ styled_line = @tui.text_line(spans: [
54
+ @tui.text_span(content: sample[:text], style: @tui.style(fg: :cyan)),
55
+ ])
56
+ line_width = styled_line.width
57
+
58
+ # Build content text with newlines
59
+ content = []
60
+ content << "Sample: #{sample[:text]}"
61
+ content << ""
62
+ content << "Display Width (text_width): #{measured_width} cells"
63
+ content << "Display Width (line.width): #{line_width} cells"
64
+ content << "Character Count: #{sample[:text].length}"
65
+ content << ""
66
+ content << sample[:desc]
67
+ text = content.join("\n")
68
+
69
+ widget = @tui.paragraph(
70
+ text:,
71
+ block: @tui.block(
72
+ title: "Text Width Calculator",
73
+ borders: [:all],
74
+ border_color: "cyan"
75
+ ),
76
+ alignment: :left
77
+ )
78
+
79
+ frame.render_widget(widget, area)
80
+ end
81
+
82
+ private def render_controls(frame, area)
83
+ info = "Sample #{@selected_index + 1}/#{@text_samples.length}: #{@text_samples[@selected_index][:label]}"
84
+ controls = "↑/↓ Select q Quit"
85
+ text = "#{info}\n#{controls}"
86
+
87
+ widget = @tui.paragraph(
88
+ text:,
89
+ block: @tui.block(borders: [:top], border_color: "gray"),
90
+ alignment: :center
91
+ )
92
+
93
+ frame.render_widget(widget, area)
94
+ end
95
+
96
+ private def handle_input
97
+ event = @tui.poll_event
98
+ case event
99
+ in { type: :key, code: "q" }
100
+ :quit
101
+ in { type: :key, code: "up" }
102
+ @selected_index = (@selected_index - 1) % @text_samples.length
103
+ nil
104
+ in { type: :key, code: "down" }
105
+ @selected_index = (@selected_index + 1) % @text_samples.length
106
+ nil
107
+ else
108
+ nil
109
+ end
110
+ end
111
+ end
112
+
113
+ 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.7.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.7.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"