ratatui_ruby 0.3.1 → 0.4.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 (300) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +14 -12
  3. data/.builds/ruby-3.3.yml +14 -12
  4. data/.builds/ruby-3.4.yml +14 -12
  5. data/.builds/ruby-4.0.0.yml +14 -12
  6. data/AGENTS.md +54 -13
  7. data/CHANGELOG.md +186 -1
  8. data/README.md +17 -15
  9. data/doc/application_architecture.md +116 -0
  10. data/doc/application_testing.md +12 -7
  11. data/doc/contributors/better_dx.md +543 -0
  12. data/doc/contributors/design/ruby_frontend.md +1 -1
  13. data/doc/contributors/developing_examples.md +203 -0
  14. data/doc/contributors/documentation_style.md +97 -0
  15. data/doc/contributors/dwim_dx.md +366 -0
  16. data/doc/contributors/example_analysis.md +82 -0
  17. data/doc/custom.css +14 -0
  18. data/doc/event_handling.md +119 -0
  19. data/doc/images/all_events.png +0 -0
  20. data/doc/images/analytics.png +0 -0
  21. data/doc/images/block_padding.png +0 -0
  22. data/doc/images/block_titles.png +0 -0
  23. data/doc/images/box_demo.png +0 -0
  24. data/doc/images/calendar_demo.png +0 -0
  25. data/doc/images/cell_demo.png +0 -0
  26. data/doc/images/chart_demo.png +0 -0
  27. data/doc/images/custom_widget.png +0 -0
  28. data/doc/images/flex_layout.png +0 -0
  29. data/doc/images/gauge_demo.png +0 -0
  30. data/doc/images/hit_test.png +0 -0
  31. data/doc/images/line_gauge_demo.png +0 -0
  32. data/doc/images/list_demo.png +0 -0
  33. data/doc/images/list_styles.png +0 -0
  34. data/doc/images/login_form.png +0 -0
  35. data/doc/images/map_demo.png +0 -0
  36. data/doc/images/mouse_events.png +0 -0
  37. data/doc/images/popup_demo.png +0 -0
  38. data/doc/images/quickstart_dsl.png +0 -0
  39. data/doc/images/quickstart_lifecycle.png +0 -0
  40. data/doc/images/ratatui_logo_demo.png +0 -0
  41. data/doc/images/readme_usage.png +0 -0
  42. data/doc/images/rich_text.png +0 -0
  43. data/doc/images/scroll_text.png +0 -0
  44. data/doc/images/scrollbar_demo.png +0 -0
  45. data/doc/images/sparkline_demo.png +0 -0
  46. data/doc/images/table_flex.png +0 -0
  47. data/doc/images/table_select.png +0 -0
  48. data/doc/images/widget_style_colors.png +0 -0
  49. data/doc/index.md +1 -0
  50. data/doc/interactive_design.md +121 -0
  51. data/doc/quickstart.md +147 -72
  52. data/examples/all_events/app.rb +169 -0
  53. data/examples/all_events/app.rbs +7 -0
  54. data/examples/all_events/test_app.rb +139 -0
  55. data/examples/analytics/app.rb +258 -0
  56. data/examples/analytics/app.rbs +7 -0
  57. data/examples/analytics/test_app.rb +132 -0
  58. data/examples/block_padding/app.rb +63 -0
  59. data/examples/block_padding/app.rbs +7 -0
  60. data/examples/block_padding/test_app.rb +31 -0
  61. data/examples/block_titles/app.rb +61 -0
  62. data/examples/block_titles/app.rbs +7 -0
  63. data/examples/block_titles/test_app.rb +34 -0
  64. data/examples/box_demo/app.rb +216 -0
  65. data/examples/box_demo/app.rbs +7 -0
  66. data/examples/box_demo/test_app.rb +88 -0
  67. data/examples/calendar_demo/app.rb +101 -0
  68. data/examples/calendar_demo/app.rbs +7 -0
  69. data/examples/calendar_demo/test_app.rb +108 -0
  70. data/examples/cell_demo/app.rb +108 -0
  71. data/examples/cell_demo/app.rbs +7 -0
  72. data/examples/cell_demo/test_app.rb +36 -0
  73. data/examples/chart_demo/app.rb +203 -0
  74. data/examples/chart_demo/app.rbs +7 -0
  75. data/examples/chart_demo/test_app.rb +102 -0
  76. data/examples/custom_widget/app.rb +51 -0
  77. data/examples/custom_widget/app.rbs +7 -0
  78. data/examples/custom_widget/test_app.rb +30 -0
  79. data/examples/flex_layout/app.rb +156 -0
  80. data/examples/flex_layout/app.rbs +7 -0
  81. data/examples/flex_layout/test_app.rb +65 -0
  82. data/examples/gauge_demo/app.rb +182 -0
  83. data/examples/gauge_demo/app.rbs +7 -0
  84. data/examples/gauge_demo/test_app.rb +120 -0
  85. data/examples/hit_test/app.rb +175 -0
  86. data/examples/hit_test/app.rbs +7 -0
  87. data/examples/hit_test/test_app.rb +102 -0
  88. data/examples/line_gauge_demo/app.rb +190 -0
  89. data/examples/line_gauge_demo/app.rbs +7 -0
  90. data/examples/line_gauge_demo/test_app.rb +129 -0
  91. data/examples/list_demo/app.rb +253 -0
  92. data/examples/list_demo/app.rbs +12 -0
  93. data/examples/list_demo/test_app.rb +237 -0
  94. data/examples/list_styles/app.rb +140 -0
  95. data/examples/list_styles/app.rbs +7 -0
  96. data/examples/list_styles/test_app.rb +157 -0
  97. data/examples/{login_form.rb → login_form/app.rb} +12 -16
  98. data/examples/login_form/app.rbs +7 -0
  99. data/examples/login_form/test_app.rb +51 -0
  100. data/examples/map_demo/app.rb +90 -0
  101. data/examples/map_demo/app.rbs +7 -0
  102. data/examples/map_demo/test_app.rb +149 -0
  103. data/examples/{mouse_events.rb → mouse_events/app.rb} +29 -27
  104. data/examples/mouse_events/app.rbs +7 -0
  105. data/examples/mouse_events/test_app.rb +53 -0
  106. data/examples/{popup_demo.rb → popup_demo/app.rb} +15 -17
  107. data/examples/popup_demo/app.rbs +7 -0
  108. data/examples/{test_popup_demo.rb → popup_demo/test_app.rb} +18 -26
  109. data/examples/quickstart_dsl/app.rb +36 -0
  110. data/examples/quickstart_dsl/app.rbs +7 -0
  111. data/examples/quickstart_dsl/test_app.rb +29 -0
  112. data/examples/quickstart_lifecycle/app.rb +39 -0
  113. data/examples/quickstart_lifecycle/app.rbs +7 -0
  114. data/examples/quickstart_lifecycle/test_app.rb +29 -0
  115. data/examples/ratatui_logo_demo/app.rb +79 -0
  116. data/examples/ratatui_logo_demo/app.rbs +7 -0
  117. data/examples/ratatui_logo_demo/test_app.rb +51 -0
  118. data/examples/ratatui_mascot_demo/app.rb +84 -0
  119. data/examples/ratatui_mascot_demo/app.rbs +7 -0
  120. data/examples/ratatui_mascot_demo/test_app.rb +47 -0
  121. data/examples/readme_usage/app.rb +29 -0
  122. data/examples/readme_usage/app.rbs +7 -0
  123. data/examples/readme_usage/test_app.rb +29 -0
  124. data/examples/rich_text/app.rb +141 -0
  125. data/examples/rich_text/app.rbs +7 -0
  126. data/examples/rich_text/test_app.rb +166 -0
  127. data/examples/scroll_text/app.rb +103 -0
  128. data/examples/scroll_text/app.rbs +7 -0
  129. data/examples/scroll_text/test_app.rb +110 -0
  130. data/examples/scrollbar_demo/app.rb +143 -0
  131. data/examples/scrollbar_demo/app.rbs +7 -0
  132. data/examples/scrollbar_demo/test_app.rb +77 -0
  133. data/examples/sparkline_demo/app.rb +240 -0
  134. data/examples/sparkline_demo/app.rbs +10 -0
  135. data/examples/sparkline_demo/test_app.rb +107 -0
  136. data/examples/table_flex/app.rb +65 -0
  137. data/examples/table_flex/app.rbs +7 -0
  138. data/examples/table_flex/test_app.rb +36 -0
  139. data/examples/table_select/app.rb +198 -0
  140. data/examples/table_select/app.rbs +7 -0
  141. data/examples/table_select/test_app.rb +180 -0
  142. data/examples/widget_style_colors/app.rb +104 -0
  143. data/examples/widget_style_colors/app.rbs +14 -0
  144. data/examples/widget_style_colors/test_app.rb +48 -0
  145. data/ext/ratatui_ruby/Cargo.lock +889 -115
  146. data/ext/ratatui_ruby/Cargo.toml +4 -3
  147. data/ext/ratatui_ruby/clippy.toml +7 -0
  148. data/ext/ratatui_ruby/extconf.rb +7 -0
  149. data/ext/ratatui_ruby/src/events.rs +218 -229
  150. data/ext/ratatui_ruby/src/lib.rs +38 -10
  151. data/ext/ratatui_ruby/src/rendering.rs +90 -10
  152. data/ext/ratatui_ruby/src/style.rs +281 -98
  153. data/ext/ratatui_ruby/src/terminal.rs +119 -25
  154. data/ext/ratatui_ruby/src/text.rs +171 -0
  155. data/ext/ratatui_ruby/src/widgets/barchart.rs +97 -24
  156. data/ext/ratatui_ruby/src/widgets/block.rs +31 -3
  157. data/ext/ratatui_ruby/src/widgets/calendar.rs +45 -44
  158. data/ext/ratatui_ruby/src/widgets/canvas.rs +46 -29
  159. data/ext/ratatui_ruby/src/widgets/chart.rs +69 -27
  160. data/ext/ratatui_ruby/src/widgets/clear.rs +3 -1
  161. data/ext/ratatui_ruby/src/widgets/gauge.rs +11 -4
  162. data/ext/ratatui_ruby/src/widgets/layout.rs +218 -15
  163. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +92 -0
  164. data/ext/ratatui_ruby/src/widgets/list.rs +91 -11
  165. data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
  166. data/ext/ratatui_ruby/src/widgets/overlay.rs +3 -2
  167. data/ext/ratatui_ruby/src/widgets/paragraph.rs +35 -13
  168. data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +29 -0
  169. data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +44 -0
  170. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +59 -7
  171. data/ext/ratatui_ruby/src/widgets/sparkline.rs +70 -6
  172. data/ext/ratatui_ruby/src/widgets/table.rs +173 -64
  173. data/ext/ratatui_ruby/src/widgets/tabs.rs +105 -5
  174. data/lib/ratatui_ruby/cell.rb +166 -0
  175. data/lib/ratatui_ruby/event/focus_gained.rb +49 -0
  176. data/lib/ratatui_ruby/event/focus_lost.rb +50 -0
  177. data/lib/ratatui_ruby/event/key.rb +211 -0
  178. data/lib/ratatui_ruby/event/mouse.rb +124 -0
  179. data/lib/ratatui_ruby/event/paste.rb +71 -0
  180. data/lib/ratatui_ruby/event/resize.rb +80 -0
  181. data/lib/ratatui_ruby/event.rb +79 -0
  182. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +45 -0
  183. data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +27 -0
  184. data/lib/ratatui_ruby/schema/bar_chart.rb +228 -19
  185. data/lib/ratatui_ruby/schema/block.rb +186 -14
  186. data/lib/ratatui_ruby/schema/calendar.rb +74 -17
  187. data/lib/ratatui_ruby/schema/canvas.rb +215 -48
  188. data/lib/ratatui_ruby/schema/center.rb +49 -11
  189. data/lib/ratatui_ruby/schema/chart.rb +151 -41
  190. data/lib/ratatui_ruby/schema/clear.rb +41 -72
  191. data/lib/ratatui_ruby/schema/constraint.rb +82 -22
  192. data/lib/ratatui_ruby/schema/cursor.rb +27 -9
  193. data/lib/ratatui_ruby/schema/draw.rb +53 -0
  194. data/lib/ratatui_ruby/schema/gauge.rb +59 -15
  195. data/lib/ratatui_ruby/schema/layout.rb +95 -13
  196. data/lib/ratatui_ruby/schema/line_gauge.rb +78 -0
  197. data/lib/ratatui_ruby/schema/list.rb +93 -19
  198. data/lib/ratatui_ruby/schema/overlay.rb +34 -8
  199. data/lib/ratatui_ruby/schema/paragraph.rb +87 -30
  200. data/lib/ratatui_ruby/schema/ratatui_logo.rb +25 -0
  201. data/lib/ratatui_ruby/schema/ratatui_mascot.rb +29 -0
  202. data/lib/ratatui_ruby/schema/rect.rb +64 -15
  203. data/lib/ratatui_ruby/schema/scrollbar.rb +132 -24
  204. data/lib/ratatui_ruby/schema/shape/label.rb +66 -0
  205. data/lib/ratatui_ruby/schema/sparkline.rb +122 -15
  206. data/lib/ratatui_ruby/schema/style.rb +49 -21
  207. data/lib/ratatui_ruby/schema/table.rb +119 -21
  208. data/lib/ratatui_ruby/schema/tabs.rb +75 -13
  209. data/lib/ratatui_ruby/schema/text.rb +90 -0
  210. data/lib/ratatui_ruby/session.rb +146 -0
  211. data/lib/ratatui_ruby/test_helper.rb +156 -13
  212. data/lib/ratatui_ruby/version.rb +1 -1
  213. data/lib/ratatui_ruby.rb +143 -23
  214. data/sig/ratatui_ruby/event.rbs +69 -0
  215. data/sig/ratatui_ruby/ratatui_ruby.rbs +2 -1
  216. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +16 -0
  217. data/sig/ratatui_ruby/schema/bar_chart/bar_group.rbs +13 -0
  218. data/sig/ratatui_ruby/schema/bar_chart.rbs +20 -2
  219. data/sig/ratatui_ruby/schema/block.rbs +5 -4
  220. data/sig/ratatui_ruby/schema/calendar.rbs +6 -2
  221. data/sig/ratatui_ruby/schema/canvas.rbs +52 -39
  222. data/sig/ratatui_ruby/schema/center.rbs +3 -3
  223. data/sig/ratatui_ruby/schema/chart.rbs +8 -5
  224. data/sig/ratatui_ruby/schema/constraint.rbs +8 -5
  225. data/sig/ratatui_ruby/schema/cursor.rbs +1 -1
  226. data/sig/ratatui_ruby/schema/draw.rbs +23 -0
  227. data/sig/ratatui_ruby/schema/gauge.rbs +4 -2
  228. data/sig/ratatui_ruby/schema/layout.rbs +11 -1
  229. data/sig/ratatui_ruby/schema/line_gauge.rbs +16 -0
  230. data/sig/ratatui_ruby/schema/list.rbs +5 -1
  231. data/sig/ratatui_ruby/schema/paragraph.rbs +4 -1
  232. data/{lib/ratatui_ruby/output.rb → sig/ratatui_ruby/schema/ratatui_logo.rbs} +3 -2
  233. data/sig/ratatui_ruby/{buffer.rbs → schema/ratatui_mascot.rbs} +4 -3
  234. data/sig/ratatui_ruby/schema/rect.rbs +2 -1
  235. data/sig/ratatui_ruby/schema/scrollbar.rbs +18 -2
  236. data/sig/ratatui_ruby/schema/sparkline.rbs +6 -2
  237. data/sig/ratatui_ruby/schema/table.rbs +8 -1
  238. data/sig/ratatui_ruby/schema/tabs.rbs +5 -1
  239. data/sig/ratatui_ruby/schema/text.rbs +22 -0
  240. data/tasks/resources/build.yml.erb +13 -11
  241. data/tasks/terminal_preview/app_screenshot.rb +35 -0
  242. data/tasks/terminal_preview/crash_report.rb +54 -0
  243. data/tasks/terminal_preview/example_app.rb +25 -0
  244. data/tasks/terminal_preview/launcher_script.rb +48 -0
  245. data/tasks/terminal_preview/preview_collection.rb +60 -0
  246. data/tasks/terminal_preview/preview_timing.rb +22 -0
  247. data/tasks/terminal_preview/safety_confirmation.rb +58 -0
  248. data/tasks/terminal_preview/saved_screenshot.rb +55 -0
  249. data/tasks/terminal_preview/system_appearance.rb +11 -0
  250. data/tasks/terminal_preview/terminal_window.rb +138 -0
  251. data/tasks/terminal_preview/window_id.rb +14 -0
  252. data/tasks/terminal_preview.rake +28 -0
  253. data/tasks/test.rake +1 -1
  254. metadata +174 -53
  255. data/doc/images/examples-analytics.rb.png +0 -0
  256. data/doc/images/examples-box_demo.rb.png +0 -0
  257. data/doc/images/examples-calendar_demo.rb.png +0 -0
  258. data/doc/images/examples-chart_demo.rb.png +0 -0
  259. data/doc/images/examples-custom_widget.rb.png +0 -0
  260. data/doc/images/examples-dashboard.rb.png +0 -0
  261. data/doc/images/examples-list_styles.rb.png +0 -0
  262. data/doc/images/examples-login_form.rb.png +0 -0
  263. data/doc/images/examples-map_demo.rb.png +0 -0
  264. data/doc/images/examples-mouse_events.rb.png +0 -0
  265. data/doc/images/examples-popup_demo.rb.gif +0 -0
  266. data/doc/images/examples-quickstart_lifecycle.rb.png +0 -0
  267. data/doc/images/examples-scroll_text.rb.png +0 -0
  268. data/doc/images/examples-scrollbar_demo.rb.png +0 -0
  269. data/doc/images/examples-stock_ticker.rb.png +0 -0
  270. data/doc/images/examples-system_monitor.rb.png +0 -0
  271. data/doc/images/examples-table_select.rb.png +0 -0
  272. data/examples/analytics.rb +0 -88
  273. data/examples/box_demo.rb +0 -71
  274. data/examples/calendar_demo.rb +0 -55
  275. data/examples/chart_demo.rb +0 -84
  276. data/examples/custom_widget.rb +0 -43
  277. data/examples/dashboard.rb +0 -72
  278. data/examples/list_styles.rb +0 -66
  279. data/examples/map_demo.rb +0 -58
  280. data/examples/quickstart_dsl.rb +0 -30
  281. data/examples/quickstart_lifecycle.rb +0 -40
  282. data/examples/readme_usage.rb +0 -21
  283. data/examples/scroll_text.rb +0 -74
  284. data/examples/scrollbar_demo.rb +0 -75
  285. data/examples/stock_ticker.rb +0 -93
  286. data/examples/system_monitor.rb +0 -94
  287. data/examples/table_select.rb +0 -70
  288. data/examples/test_analytics.rb +0 -65
  289. data/examples/test_box_demo.rb +0 -38
  290. data/examples/test_calendar_demo.rb +0 -66
  291. data/examples/test_dashboard.rb +0 -38
  292. data/examples/test_list_styles.rb +0 -61
  293. data/examples/test_login_form.rb +0 -63
  294. data/examples/test_map_demo.rb +0 -100
  295. data/examples/test_scroll_text.rb +0 -130
  296. data/examples/test_stock_ticker.rb +0 -39
  297. data/examples/test_system_monitor.rb +0 -40
  298. data/examples/test_table_select.rb +0 -37
  299. data/ext/ratatui_ruby/src/buffer.rs +0 -54
  300. data/lib/ratatui_ruby/dsl.rb +0 -64
@@ -2,38 +2,118 @@
2
2
  // SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
4
  use crate::style::parse_block;
5
+ use bumpalo::Bump;
5
6
  use magnus::{prelude::*, Error, Value};
6
7
  use ratatui::{layout::Rect, text::Line, widgets::Tabs, Frame};
7
8
 
8
9
  pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
10
+ let bump = Bump::new();
11
+ let tabs = create_tabs(node, &bump)?;
12
+ frame.render_widget(tabs, area);
13
+ Ok(())
14
+ }
15
+
16
+ use crate::text::parse_line;
17
+
18
+ fn create_tabs<'a>(node: Value, bump: &'a Bump) -> Result<Tabs<'a>, Error> {
9
19
  let ruby = magnus::Ruby::get().unwrap();
10
20
  let titles_val: Value = node.funcall("titles", ())?;
11
21
  let selected_index: usize = node.funcall("selected_index", ())?;
12
22
  let block_val: Value = node.funcall("block", ())?;
23
+ let divider_val: Value = node.funcall("divider", ())?;
24
+ let highlight_style_val: Value = node.funcall("highlight_style", ())?;
25
+ let padding_left: usize = node.funcall("padding_left", ())?;
26
+ let padding_right: usize = node.funcall("padding_right", ())?;
13
27
 
14
28
  let titles_array = magnus::RArray::from_value(titles_val)
15
29
  .ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for titles"))?;
16
30
 
17
31
  let mut titles = Vec::new();
18
32
  for i in 0..titles_array.len() {
19
- let title: String = titles_array.entry(i as isize)?;
20
- titles.push(Line::from(title));
33
+ let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
34
+ let val: Value = titles_array.entry(index)?;
35
+ if let Ok(line) = parse_line(val) {
36
+ titles.push(line);
37
+ } else {
38
+ let s: String = String::try_convert(val)?;
39
+ titles.push(Line::from(s));
40
+ }
21
41
  }
22
42
 
23
43
  let mut tabs = Tabs::new(titles).select(selected_index);
24
44
 
45
+ if !divider_val.is_nil() {
46
+ let divider: String = divider_val.funcall("to_s", ())?;
47
+ tabs = tabs.divider(divider);
48
+ }
49
+
50
+ if !highlight_style_val.is_nil() {
51
+ let style = crate::style::parse_style(highlight_style_val)?;
52
+ tabs = tabs.highlight_style(style);
53
+ }
54
+
55
+ let style_val: Value = node.funcall("style", ())?;
56
+ if !style_val.is_nil() {
57
+ tabs = tabs.style(crate::style::parse_style(style_val)?);
58
+ }
59
+
25
60
  if !block_val.is_nil() {
26
- tabs = tabs.block(parse_block(block_val)?);
61
+ tabs = tabs.block(parse_block(block_val, bump)?);
27
62
  }
28
63
 
29
- frame.render_widget(tabs, area);
30
- Ok(())
64
+ if padding_left > 0 || padding_right > 0 {
65
+ let left_str = " ".repeat(padding_left);
66
+ let right_str = " ".repeat(padding_right);
67
+ tabs = tabs.padding(left_str, right_str);
68
+ }
69
+
70
+ Ok(tabs)
71
+ }
72
+
73
+ pub fn width(node: Value) -> Result<usize, Error> {
74
+ let ruby = magnus::Ruby::get().unwrap();
75
+ let titles_val: Value = node.funcall("titles", ())?;
76
+ let divider_val: Value = node.funcall("divider", ())?;
77
+ let padding_left: usize = node.funcall("padding_left", ())?;
78
+ let padding_right: usize = node.funcall("padding_right", ())?;
79
+
80
+ let titles_array = magnus::RArray::from_value(titles_val)
81
+ .ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for titles"))?;
82
+
83
+ let mut total_width = padding_left + padding_right;
84
+
85
+ let mut titles_count = 0;
86
+ for i in 0..titles_array.len() {
87
+ let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
88
+ let val: Value = titles_array.entry(index)?;
89
+ let line_width = if let Ok(line) = parse_line(val) {
90
+ line.width()
91
+ } else {
92
+ let s: String = String::try_convert(val)?;
93
+ ratatui::text::Span::raw(s).width()
94
+ };
95
+ total_width += line_width;
96
+ titles_count += 1;
97
+ }
98
+
99
+ if titles_count > 1 {
100
+ let divider_width = if !divider_val.is_nil() {
101
+ let d: String = divider_val.funcall("to_s", ())?;
102
+ ratatui::text::Span::raw(d).width()
103
+ } else {
104
+ 1 // Default divider is "|"
105
+ };
106
+ total_width += (titles_count - 1) * divider_width;
107
+ }
108
+
109
+ Ok(total_width)
31
110
  }
32
111
 
33
112
  #[cfg(test)]
34
113
  mod tests {
35
114
  use super::*;
36
115
  use ratatui::buffer::Buffer;
116
+ use ratatui::style::{Color, Modifier, Style};
37
117
  use ratatui::text::Line;
38
118
  use ratatui::widgets::{Tabs, Widget};
39
119
 
@@ -49,4 +129,24 @@ mod tests {
49
129
  assert!(content.contains("Tab2"));
50
130
  assert!(content.contains('|'));
51
131
  }
132
+
133
+ #[test]
134
+ fn test_tabs_highlight_style() {
135
+ let titles = vec![Line::from("Tab1"), Line::from("Tab2")];
136
+ let highlight_style = Style::default().fg(Color::Red).add_modifier(Modifier::BOLD);
137
+ let tabs = Tabs::new(titles)
138
+ .select(0)
139
+ .highlight_style(highlight_style);
140
+
141
+ let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
142
+ tabs.render(Rect::new(0, 0, 15, 1), &mut buf);
143
+
144
+ // Check the first cell of the first tab (which is selected)
145
+ // " Tab1 "
146
+ // Index 1 should be 'T' with Red+Bold
147
+ let cell = &buf.content()[1];
148
+ assert_eq!(cell.symbol(), "T");
149
+ assert_eq!(cell.fg, Color::Red);
150
+ assert!(cell.modifier.contains(Modifier::BOLD));
151
+ }
52
152
  }
@@ -0,0 +1,166 @@
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
+ module RatatuiRuby
7
+ # Represents a single cell in the terminal buffer.
8
+ #
9
+ # A terminal grid is made of cells. Each cell contains a character (symbol) and styling (colors, modifiers).
10
+ # When testing, you often need to verify that a specific cell renders correctly.
11
+ #
12
+ # This object encapsulates that state. It provides predicate methods for modifiers, making assertions readable.
13
+ #
14
+ # Use it to inspect the visual state of your application in tests.
15
+ #
16
+ # === Examples
17
+ #
18
+ # cell = RatatuiRuby.get_cell_at(0, 0)
19
+ # cell.char # => "H"
20
+ # cell.fg # => :red
21
+ # cell.bold? # => true
22
+ #
23
+ class Cell
24
+ # The character displayed in the cell.
25
+ #
26
+ # Named to match Ratatui's Cell::symbol() method.
27
+ attr_reader :symbol
28
+
29
+ # Alias for Rubyists who prefer a shorter name.
30
+ alias char symbol
31
+
32
+ # The foreground color of the cell (e.g., :red, :blue, "#ff0000").
33
+ attr_reader :fg
34
+
35
+ # The background color of the cell (e.g., :black, nil).
36
+ attr_reader :bg
37
+
38
+ # The list of active modifiers (e.g., ["bold", "italic"]).
39
+ attr_reader :modifiers
40
+
41
+ # Returns an empty cell (space character, no styles).
42
+ #
43
+ # === Example
44
+ #
45
+ # Cell.empty # => #<RatatuiRuby::Cell char=" ">
46
+ #
47
+ def self.empty
48
+ new(symbol: " ", fg: nil, bg: nil, modifiers: [])
49
+ end
50
+
51
+ # Returns a default cell (alias for empty).
52
+ #
53
+ # === Example
54
+ #
55
+ # Cell.default # => #<RatatuiRuby::Cell char=" ">
56
+ #
57
+ def self.default
58
+ empty
59
+ end
60
+
61
+ # Returns a cell with a specific character and no styles.
62
+ #
63
+ # [symbol] String (single character).
64
+ #
65
+ # === Example
66
+ #
67
+ # Cell.symbol("X") # => #<RatatuiRuby::Cell symbol="X">
68
+ #
69
+ def self.symbol(symbol)
70
+ new(symbol: symbol, fg: nil, bg: nil, modifiers: [])
71
+ end
72
+
73
+ # Alias for Rubyists who prefer a shorter name.
74
+ def self.char(char)
75
+ symbol(char)
76
+ end
77
+
78
+ # Creates a new Cell.
79
+ #
80
+ # [symbol] String (single character). Aliased as <tt>char:</tt>.
81
+ # [fg] Symbol or String (nullable).
82
+ # [bg] Symbol or String (nullable).
83
+ # [modifiers] Array of Strings.
84
+ def initialize(symbol: nil, char: nil, fg: nil, bg: nil, modifiers: [])
85
+ @symbol = symbol || char || " "
86
+ @fg = fg
87
+ @bg = bg
88
+ @modifiers = modifiers.freeze
89
+ freeze
90
+ end
91
+
92
+ # Returns true if the cell has the bold modifier.
93
+ def bold?
94
+ modifiers.include?("bold")
95
+ end
96
+
97
+ # Returns true if the cell has the dim modifier.
98
+ def dim?
99
+ modifiers.include?("dim")
100
+ end
101
+
102
+ # Returns true if the cell has the italic modifier.
103
+ def italic?
104
+ modifiers.include?("italic")
105
+ end
106
+
107
+ # Returns true if the cell has the underlined modifier.
108
+ def underlined?
109
+ modifiers.include?("underlined")
110
+ end
111
+
112
+ # Returns true if the cell has the slow_blink modifier.
113
+ def slow_blink?
114
+ modifiers.include?("slow_blink")
115
+ end
116
+
117
+ # Returns true if the cell has the rapid_blink modifier.
118
+ def rapid_blink?
119
+ modifiers.include?("rapid_blink")
120
+ end
121
+
122
+ # Returns true if the cell has the reversed modifier.
123
+ def reversed?
124
+ modifiers.include?("reversed")
125
+ end
126
+
127
+ # Returns true if the cell has the hidden modifier.
128
+ def hidden?
129
+ modifiers.include?("hidden")
130
+ end
131
+
132
+ # Returns true if the cell has the crossed_out modifier.
133
+ def crossed_out?
134
+ modifiers.include?("crossed_out")
135
+ end
136
+
137
+ # Checks equality with another Cell.
138
+ def ==(other)
139
+ other.is_a?(Cell) &&
140
+ char == other.char &&
141
+ fg == other.fg &&
142
+ bg == other.bg &&
143
+ modifiers == other.modifiers
144
+ end
145
+
146
+ # Returns a string representation of the cell.
147
+ def inspect
148
+ parts = ["symbol=#{symbol.inspect}"]
149
+ parts << "fg=#{fg.inspect}" if fg
150
+ parts << "bg=#{bg.inspect}" if bg
151
+ parts << "modifiers=#{modifiers.inspect}" unless modifiers.empty?
152
+ "#<#{self.class} #{parts.join(" ")}>"
153
+ end
154
+
155
+ # Returns the cell's character.
156
+ def to_s
157
+ symbol
158
+ end
159
+
160
+ # Support for pattern matching.
161
+ # Supports both <tt>:symbol</tt> and <tt>:char</tt> keys.
162
+ def deconstruct_keys(keys)
163
+ { symbol: symbol, char: symbol, fg: fg, bg: bg, modifiers: modifiers }
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,49 @@
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
+ module RatatuiRuby
7
+ class Event
8
+ # Signals that the application is now active.
9
+ #
10
+ # The user interacts with many windows. Your application needs to know when it has their attention.
11
+ #
12
+ # This event confirms visibility. It fires when the terminal window moves to the foreground.
13
+ #
14
+ # Use it to resume paused activities. Restart animations. Refresh data. The user is watching.
15
+ #
16
+ # Only supported by some terminals (e.g. iTerm2, Kitty, newer xterm).
17
+ #
18
+ # === Example
19
+ #
20
+ # if event.focus_gained?
21
+ # puts "Focus gained"
22
+ # end
23
+ class FocusGained < Event
24
+ # Returns true for FocusGained events.
25
+ #
26
+ # event.focus_gained? # => true
27
+ # event.key? # => false
28
+ def focus_gained?
29
+ true
30
+ end
31
+
32
+ # Deconstructs the event for pattern matching.
33
+ #
34
+ # case event
35
+ # in type: :focus_gained
36
+ # puts "Application gained focus"
37
+ # end
38
+ def deconstruct_keys(keys)
39
+ { type: :focus_gained }
40
+ end
41
+
42
+ ##
43
+ # Compares this event with another for equality.
44
+ def ==(other)
45
+ other.is_a?(FocusGained)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,50 @@
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
+ module RatatuiRuby
7
+ class Event
8
+ # Signals that the application is in the background.
9
+ #
10
+ # The user has switched context. Your application is no longer the primary focus.
11
+ #
12
+ # This event warns of inactivity. It fires when the terminal window loses focus.
13
+ #
14
+ # Respond by conserving resources. Pause animations. Stop heavy polling. dim the UI to
15
+ # indicate a background state.
16
+ #
17
+ # Only supported by some terminals (e.g. iTerm2, Kitty, newer xterm).
18
+ #
19
+ # === Example
20
+ #
21
+ # if event.focus_lost?
22
+ # puts "Focus lost"
23
+ # end
24
+ class FocusLost < Event
25
+ # Returns true for FocusLost events.
26
+ #
27
+ # event.focus_lost? # => true
28
+ # event.key? # => false
29
+ def focus_lost?
30
+ true
31
+ end
32
+
33
+ # Deconstructs the event for pattern matching.
34
+ #
35
+ # case event
36
+ # in type: :focus_lost
37
+ # puts "Application lost focus"
38
+ # end
39
+ def deconstruct_keys(keys)
40
+ { type: :focus_lost }
41
+ end
42
+
43
+ ##
44
+ # Compares this event with another for equality.
45
+ def ==(other)
46
+ other.is_a?(FocusLost)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,211 @@
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
+ module RatatuiRuby
7
+ class Event
8
+ # Captures a keyboard interaction.
9
+ #
10
+ # The keyboard is the primary interface for your terminal application. Raw key codes are often cryptic,
11
+ # and handling modifiers manually is error-prone.
12
+ #
13
+ # This event creates clarity. It encapsulates the interaction, providing a normalized +code+ and
14
+ # a list of active +modifiers+.
15
+ #
16
+ # Compare it directly to strings or symbols for rapid development, or use pattern matching for
17
+ # complex control schemes.
18
+ #
19
+ # === Examples
20
+ #
21
+ # Using predicates:
22
+ # if event.key? && event.ctrl? && event.code == "c"
23
+ # exit
24
+ # end
25
+ #
26
+ # Using symbol comparison:
27
+ # if event == :ctrl_c
28
+ # exit
29
+ # end
30
+ #
31
+ # Using pattern matching:
32
+ # case event
33
+ # in type: :key, code: "c", modifiers: ["ctrl"]
34
+ # exit
35
+ # end
36
+ class Key < Event
37
+ # The key code (e.g., <tt>"a"</tt>, <tt>"enter"</tt>, <tt>"up"</tt>).
38
+ #
39
+ # puts event.code # => "enter"
40
+ attr_reader :code
41
+
42
+ # List of active modifiers (<tt>"ctrl"</tt>, <tt>"alt"</tt>, <tt>"shift"</tt>).
43
+ #
44
+ # puts event.modifiers # => ["ctrl", "shift"]
45
+ attr_reader :modifiers
46
+
47
+ # Returns true for Key events.
48
+ #
49
+ # event.key? # => true
50
+ # event.mouse? # => false
51
+ # event.resize? # => false
52
+ def key?
53
+ true
54
+ end
55
+
56
+ # Creates a new Key event.
57
+ #
58
+ # [code]
59
+ # The key code (String).
60
+ # [modifiers]
61
+ # List of modifiers (Array<String>).
62
+ def initialize(code:, modifiers: [])
63
+ @code = code
64
+ @modifiers = modifiers.sort
65
+ end
66
+
67
+ # Compares the event with another object.
68
+ #
69
+ # - If +other+ is a +Symbol+, compares against #to_sym.
70
+ # - If +other+ is a +String+, compares against #to_s.
71
+ # - Otherwise, performs standard equality check.
72
+ # - Otherwise, compares internal state (code + modifiers).
73
+ def ==(other)
74
+ case other
75
+ when Symbol
76
+ to_sym == other
77
+ when String
78
+ to_s == other
79
+ when Key
80
+ code == other.code && modifiers == other.modifiers
81
+ else
82
+ super
83
+ end
84
+ end
85
+
86
+ # Converts the event to a Symbol representation.
87
+ #
88
+ # The format is <tt>[modifiers_]code</tt>. Modifiers are sorted alphabetically (alt, ctrl, shift)
89
+ # and joined by underscores.
90
+ #
91
+ # === Supported Keys
92
+ #
93
+ # [Standard]
94
+ # <tt>:enter</tt>, <tt>:backspace</tt>, <tt>:tab</tt>, <tt>:esc</tt>, <tt>:page_up</tt>, <tt>:page_down</tt>, <tt>:home</tt>, <tt>:end</tt>, <tt>:delete</tt>, <tt>:insert</tt>, <tt>:f1</tt>..<tt>:f12</tt>
95
+ # [Navigation]
96
+ # <tt>:up</tt>, <tt>:down</tt>, <tt>:left</tt>, <tt>:right</tt>
97
+ # [Characters]
98
+ # <tt>:a</tt>, <tt>:b</tt>, <tt>:1</tt>, <tt>:space</tt>, etc.
99
+ #
100
+ # === Modifier Examples
101
+ #
102
+ # * <tt>:ctrl_c</tt>
103
+ # * <tt>:alt_enter</tt>
104
+ # * <tt>:shift_left</tt>
105
+ # * <tt>:ctrl_alt_delete</tt>
106
+ def to_sym
107
+ mods = @modifiers.join("_")
108
+ if mods.empty?
109
+ @code.to_sym
110
+ else
111
+ "#{mods}_#{@code}".to_sym
112
+ end
113
+ end
114
+
115
+ # Converts the event to its String representation.
116
+ #
117
+ # [Printable Characters]
118
+ # Returns the character itself (e.g., <tt>"a"</tt>, <tt>"1"</tt>, <tt>" "</tt>).
119
+ # [Special Keys]
120
+ # Returns an empty string (e.g., <tt>"enter"</tt>, <tt>"up"</tt>, <tt>"f1"</tt> all return <tt>""</tt>).
121
+ # [Modifiers]
122
+ # Returns the character if printable, ignoring modifiers unless they alter the character code itself.
123
+ # Note that <tt>ctrl+c</tt> typically returns <tt>"c"</tt> as the code, so +to_s+ will return <tt>"c"</tt>.
124
+ def to_s
125
+ if text?
126
+ @code
127
+ else
128
+ ""
129
+ end
130
+ end
131
+
132
+ # Returns inspection string.
133
+ def inspect
134
+ "#<#{self.class} code=#{@code.inspect} modifiers=#{@modifiers.inspect}>"
135
+ end
136
+
137
+ # Returns true if CTRL is held.
138
+ def ctrl?
139
+ @modifiers.include?("ctrl")
140
+ end
141
+
142
+ # Returns true if ALT is held.
143
+ def alt?
144
+ @modifiers.include?("alt")
145
+ end
146
+
147
+ # Returns true if SHIFT is held.
148
+ def shift?
149
+ @modifiers.include?("shift")
150
+ end
151
+
152
+ # Returns true if the key represents a single printable character.
153
+ #
154
+ # RatatuiRuby::Event::Key.new(code: "a").text? # => true
155
+ # RatatuiRuby::Event::Key.new(code: "enter").text? # => false
156
+ # RatatuiRuby::Event::Key.new(code: "space").text? # => false ("space" is not 1 char, " " is)
157
+ def text?
158
+ @code.length == 1
159
+ end
160
+
161
+ # Returns the key as a printable character (if applicable).
162
+ #
163
+ # [Printable Characters]
164
+ # Returns the character itself (e.g., <tt>"a"</tt>, <tt>"1"</tt>, <tt>" "</tt>).
165
+ # [Special Keys]
166
+ # Returns an empty string (e.g., <tt>"enter"</tt>, <tt>"up"</tt>, <tt>"f1"</tt>).
167
+ #
168
+ # This is equivalent to +to_s+.
169
+ #
170
+ # RatatuiRuby::Event::Key.new(code: "a").char # => "a"
171
+ # RatatuiRuby::Event::Key.new(code: "enter").char # => ""
172
+ def char
173
+ to_s
174
+ end
175
+
176
+ # Supports dynamic key predicate methods via method_missing.
177
+ #
178
+ # Allows convenient checking for specific keys or key combinations:
179
+ #
180
+ # event.ctrl_c? # => true if Ctrl+C
181
+ # event.enter? # => true if Enter
182
+ # event.shift_up? # => true if Shift+Up
183
+ # event.q? # => true if "q"
184
+ #
185
+ # The method name is converted to a symbol and compared against the event.
186
+ # This works for any key code or modifier+key combination.
187
+ def method_missing(name, *args, &block)
188
+ if name.to_s.end_with?("?")
189
+ key_sym = name.to_s[0...-1].to_sym
190
+ return self == key_sym
191
+ end
192
+ super
193
+ end
194
+
195
+ # Declares that this class responds to dynamic predicate methods.
196
+ def respond_to_missing?(name, *args)
197
+ name.to_s.end_with?("?") || super
198
+ end
199
+
200
+ # Deconstructs the event for pattern matching.
201
+ #
202
+ # case event
203
+ # in type: :key, code: "c", modifiers: ["ctrl"]
204
+ # puts "Ctrl+C pressed"
205
+ # end
206
+ def deconstruct_keys(keys)
207
+ { type: :key, code: @code, modifiers: @modifiers }
208
+ end
209
+ end
210
+ end
211
+ end