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,6 +2,7 @@
2
2
  // SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
4
  use magnus::Error;
5
+ use magnus::value::ReprValue;
5
6
  use ratatui::{
6
7
  backend::{CrosstermBackend, TestBackend},
7
8
  Terminal,
@@ -11,14 +12,12 @@ use std::sync::Mutex;
11
12
 
12
13
  pub enum TerminalWrapper {
13
14
  Crossterm(Terminal<CrosstermBackend<io::Stdout>>),
14
- Test(Terminal<TestBackend>), // We don't need Mutex inside the enum variant because the global is a Mutex
15
+ Test(Terminal<TestBackend>),
15
16
  }
16
17
 
17
- lazy_static::lazy_static! {
18
- pub static ref TERMINAL: Mutex<Option<TerminalWrapper>> = Mutex::new(None);
19
- }
18
+ pub static TERMINAL: Mutex<Option<TerminalWrapper>> = Mutex::new(None);
20
19
 
21
- pub fn init_terminal() -> Result<(), Error> {
20
+ pub fn init_terminal(focus_events: bool, bracketed_paste: bool) -> Result<(), Error> {
22
21
  let ruby = magnus::Ruby::get().unwrap();
23
22
  let mut term_lock = TERMINAL.lock().unwrap();
24
23
  if term_lock.is_none() {
@@ -31,6 +30,16 @@ pub fn init_terminal() -> Result<(), Error> {
31
30
  ratatui::crossterm::event::EnableMouseCapture
32
31
  )
33
32
  .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
33
+
34
+ if focus_events {
35
+ ratatui::crossterm::execute!(stdout, ratatui::crossterm::event::EnableFocusChange)
36
+ .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
37
+ }
38
+ if bracketed_paste {
39
+ ratatui::crossterm::execute!(stdout, ratatui::crossterm::event::EnableBracketedPaste)
40
+ .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
41
+ }
42
+
34
43
  let backend = CrosstermBackend::new(stdout);
35
44
  let terminal = Terminal::new(backend)
36
45
  .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
@@ -49,30 +58,30 @@ pub fn init_test_terminal(width: u16, height: u16) -> Result<(), Error> {
49
58
  Ok(())
50
59
  }
51
60
 
52
- pub fn restore_terminal() -> Result<(), Error> {
61
+ pub fn restore_terminal() {
53
62
  let mut term_lock = TERMINAL.lock().unwrap();
54
- if let Some(TerminalWrapper::Crossterm(mut terminal)) = term_lock.take() {
55
- let _ = ratatui::crossterm::terminal::disable_raw_mode();
56
- let _ = ratatui::crossterm::execute!(
57
- terminal.backend_mut(),
58
- ratatui::crossterm::terminal::LeaveAlternateScreen,
59
- ratatui::crossterm::event::DisableMouseCapture
60
- );
63
+ if let Some(wrapper) = term_lock.take() {
64
+ match wrapper {
65
+ TerminalWrapper::Crossterm(mut t) => {
66
+ let _ = ratatui::crossterm::terminal::disable_raw_mode();
67
+ let _ = ratatui::crossterm::execute!(
68
+ t.backend_mut(),
69
+ ratatui::crossterm::terminal::LeaveAlternateScreen,
70
+ ratatui::crossterm::event::DisableMouseCapture,
71
+ ratatui::crossterm::event::DisableFocusChange,
72
+ ratatui::crossterm::event::DisableBracketedPaste
73
+ );
74
+ }
75
+ TerminalWrapper::Test(_) => {}
76
+ }
61
77
  }
62
- Ok(())
63
78
  }
64
79
 
65
80
  pub fn get_buffer_content() -> Result<String, Error> {
66
81
  let ruby = magnus::Ruby::get().unwrap();
67
82
  let term_lock = TERMINAL.lock().unwrap();
68
83
  if let Some(TerminalWrapper::Test(terminal)) = term_lock.as_ref() {
69
- // We need to access the buffer.
70
- // Since we are mocking, we can just print the buffer to a string.
71
84
  let buffer = terminal.backend().buffer();
72
- // Simple representation: each cell's symbol.
73
- // For a more complex representation we could return an array of strings.
74
- // Let's just return the full string representation for now which is useful for debugging/asserting.
75
- // Actually, let's reconstruct it line by line.
76
85
  let area = buffer.area;
77
86
  let mut result = String::new();
78
87
  for y in 0..area.height {
@@ -113,14 +122,9 @@ pub fn resize_terminal(width: u16, height: u16) -> Result<(), Error> {
113
122
  if let Some(wrapper) = term_lock.as_mut() {
114
123
  match wrapper {
115
124
  TerminalWrapper::Crossterm(_) => {
116
- // Resize happens automatically for Crossterm via signals usually,
117
- // but we can't easily force it here without OS interaction.
118
- // Ignoring for now as it's less critical for unit testing the logic.
119
125
  }
120
126
  TerminalWrapper::Test(terminal) => {
121
127
  terminal.backend_mut().resize(width, height);
122
- // Also resize the terminal wrapper itself if needed, but TestBackend resize handles the buffer.
123
- // We might need to call terminal.resize() too if Ratatui caches the size.
124
128
  if let Err(e) = terminal.resize(ratatui::layout::Rect::new(0, 0, width, height)) {
125
129
  return Err(Error::new(
126
130
  ruby.exception_runtime_error(),
@@ -132,3 +136,93 @@ pub fn resize_terminal(width: u16, height: u16) -> Result<(), Error> {
132
136
  }
133
137
  Ok(())
134
138
  }
139
+
140
+ use magnus::Value;
141
+
142
+ pub fn get_cell_at(x: u16, y: u16) -> Result<magnus::RHash, Error> {
143
+ let ruby = magnus::Ruby::get().unwrap();
144
+ let term_lock = TERMINAL.lock().unwrap();
145
+ if let Some(TerminalWrapper::Test(terminal)) = term_lock.as_ref() {
146
+ let buffer = terminal.backend().buffer();
147
+ if let Some(cell) = buffer.cell((x, y)) {
148
+ let hash = ruby.hash_new();
149
+ hash.aset("char", cell.symbol())?;
150
+ hash.aset("fg", color_to_value(cell.fg))?;
151
+ hash.aset("bg", color_to_value(cell.bg))?;
152
+ hash.aset("modifiers", modifiers_to_value(cell.modifier))?;
153
+ Ok(hash)
154
+ } else {
155
+ Err(Error::new(
156
+ ruby.exception_runtime_error(),
157
+ format!("Coordinates ({x}, {y}) out of bounds"),
158
+ ))
159
+ }
160
+ } else {
161
+ Err(Error::new(
162
+ ruby.exception_runtime_error(),
163
+ "Terminal is not initialized as TestBackend",
164
+ ))
165
+ }
166
+ }
167
+
168
+ fn color_to_value(color: ratatui::style::Color) -> Value {
169
+ let ruby = magnus::Ruby::get().unwrap();
170
+ match color {
171
+ ratatui::style::Color::Reset => ruby.qnil().as_value(),
172
+ ratatui::style::Color::Black => ruby.to_symbol("black").as_value(),
173
+ ratatui::style::Color::Red => ruby.to_symbol("red").as_value(),
174
+ ratatui::style::Color::Green => ruby.to_symbol("green").as_value(),
175
+ ratatui::style::Color::Yellow => ruby.to_symbol("yellow").as_value(),
176
+ ratatui::style::Color::Blue => ruby.to_symbol("blue").as_value(),
177
+ ratatui::style::Color::Magenta => ruby.to_symbol("magenta").as_value(),
178
+ ratatui::style::Color::Cyan => ruby.to_symbol("cyan").as_value(),
179
+ ratatui::style::Color::Gray => ruby.to_symbol("gray").as_value(),
180
+ ratatui::style::Color::DarkGray => ruby.to_symbol("dark_gray").as_value(),
181
+ ratatui::style::Color::LightRed => ruby.to_symbol("light_red").as_value(),
182
+ ratatui::style::Color::LightGreen => ruby.to_symbol("light_green").as_value(),
183
+ ratatui::style::Color::LightYellow => ruby.to_symbol("light_yellow").as_value(),
184
+ ratatui::style::Color::LightBlue => ruby.to_symbol("light_blue").as_value(),
185
+ ratatui::style::Color::LightMagenta => ruby.to_symbol("light_magenta").as_value(),
186
+ ratatui::style::Color::LightCyan => ruby.to_symbol("light_cyan").as_value(),
187
+ ratatui::style::Color::White => ruby.to_symbol("white").as_value(),
188
+ ratatui::style::Color::Rgb(r, g, b) => ruby
189
+ .str_new(&(format!("#{r:02x}{g:02x}{b:02x}")))
190
+ .as_value(),
191
+ ratatui::style::Color::Indexed(i) => ruby.to_symbol(format!("indexed_{i}")).as_value(),
192
+ }
193
+ }
194
+
195
+ fn modifiers_to_value(modifier: ratatui::style::Modifier) -> Value {
196
+ let ruby = magnus::Ruby::get().unwrap();
197
+ let ary = ruby.ary_new();
198
+
199
+ if modifier.contains(ratatui::style::Modifier::BOLD) {
200
+ let _ = ary.push(ruby.str_new("bold"));
201
+ }
202
+ if modifier.contains(ratatui::style::Modifier::ITALIC) {
203
+ let _ = ary.push(ruby.str_new("italic"));
204
+ }
205
+ if modifier.contains(ratatui::style::Modifier::DIM) {
206
+ let _ = ary.push(ruby.str_new("dim"));
207
+ }
208
+ if modifier.contains(ratatui::style::Modifier::UNDERLINED) {
209
+ let _ = ary.push(ruby.str_new("underlined"));
210
+ }
211
+ if modifier.contains(ratatui::style::Modifier::REVERSED) {
212
+ let _ = ary.push(ruby.str_new("reversed"));
213
+ }
214
+ if modifier.contains(ratatui::style::Modifier::HIDDEN) {
215
+ let _ = ary.push(ruby.str_new("hidden"));
216
+ }
217
+ if modifier.contains(ratatui::style::Modifier::CROSSED_OUT) {
218
+ let _ = ary.push(ruby.str_new("crossed_out"));
219
+ }
220
+ if modifier.contains(ratatui::style::Modifier::SLOW_BLINK) {
221
+ let _ = ary.push(ruby.str_new("slow_blink"));
222
+ }
223
+ if modifier.contains(ratatui::style::Modifier::RAPID_BLINK) {
224
+ let _ = ary.push(ruby.str_new("rapid_blink"));
225
+ }
226
+
227
+ ary.as_value()
228
+ }
@@ -0,0 +1,171 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ use crate::style::parse_style;
5
+ use magnus::{prelude::*, Error, Value};
6
+ use ratatui::text::{Line, Span};
7
+
8
+ /// Parses a Ruby value into a ratatui Text structure.
9
+ ///
10
+ /// Supports:
11
+ /// - String: Plain text without styling
12
+ /// - `Text::Span`: A single styled fragment
13
+ /// - `Text::Line`: A line composed of multiple spans
14
+ /// - Array: Array of `Text::Lines` or Strings
15
+ pub fn parse_text(value: Value) -> Result<Vec<Line<'static>>, Error> {
16
+ let ruby = magnus::Ruby::get().unwrap();
17
+
18
+ if value.is_nil() {
19
+ return Ok(vec![Line::from("")]);
20
+ }
21
+
22
+ // Check if it's a String
23
+ if let Ok(s) = String::try_convert(value) {
24
+ // Split on newlines and create a Line for each.
25
+ // We need to own the strings, so we convert each line string to a String
26
+ let lines: Vec<Line> = s.split('\n').map(|line| Line::from(line.to_string())).collect();
27
+ return if lines.is_empty() {
28
+ Ok(vec![Line::from("")])
29
+ } else {
30
+ Ok(lines)
31
+ };
32
+ }
33
+
34
+ // Check if it's an Array
35
+ if let Some(arr) = magnus::RArray::from_value(value) {
36
+ let mut lines = Vec::new();
37
+ for i in 0..arr.len() {
38
+ let ruby = magnus::Ruby::get().unwrap();
39
+ let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
40
+ let elem: Value = arr.entry(index)?;
41
+
42
+ // Try to convert to String
43
+ if let Ok(s) = String::try_convert(elem) {
44
+ lines.push(Line::from(s));
45
+ } else if let Ok(line) = parse_line(elem) {
46
+ lines.push(line);
47
+ } else if let Ok(span) = parse_span(elem) {
48
+ lines.push(Line::from(vec![span]));
49
+ }
50
+ }
51
+ return if lines.is_empty() {
52
+ Ok(vec![Line::from("")])
53
+ } else {
54
+ Ok(lines)
55
+ };
56
+ }
57
+
58
+ // Try to parse as Line
59
+ if let Ok(line) = parse_line(value) {
60
+ return Ok(vec![line]);
61
+ }
62
+
63
+ // Try to parse as Span
64
+ if let Ok(span) = parse_span(value) {
65
+ return Ok(vec![Line::from(vec![span])]);
66
+ }
67
+
68
+ // Fallback: try to convert to string
69
+ match String::try_convert(value) {
70
+ Ok(s) => {
71
+ let lines: Vec<Line> = s.split('\n').map(|line| Line::from(line.to_string())).collect();
72
+ if lines.is_empty() {
73
+ Ok(vec![Line::from("")])
74
+ } else {
75
+ Ok(lines)
76
+ }
77
+ }
78
+ Err(_) => Err(Error::new(
79
+ ruby.exception_type_error(),
80
+ "expected String, Text::Span, Text::Line, or Array of Text::Lines/Spans",
81
+ )),
82
+ }
83
+ }
84
+
85
+ /// Parses a Ruby Span object into a ratatui Span.
86
+ fn parse_span(value: Value) -> Result<Span<'static>, Error> {
87
+ let ruby = magnus::Ruby::get().unwrap();
88
+
89
+ // Get class name
90
+ let class_obj: Value = value.funcall("class", ())?;
91
+ let class_name: String = class_obj.funcall("name", ())?;
92
+
93
+ if !class_name.contains("Span") {
94
+ return Err(Error::new(
95
+ ruby.exception_type_error(),
96
+ "expected a Text::Span object",
97
+ ));
98
+ }
99
+
100
+ // Extract content and style from the Ruby Span
101
+ let content: Value = value.funcall("content", ())?;
102
+ let style_val: Value = value.funcall("style", ())?;
103
+
104
+ let content_str: String = content.funcall("to_s", ())?;
105
+ let style = parse_style(style_val)?;
106
+
107
+ Ok(Span::styled(content_str, style))
108
+ }
109
+
110
+ /// Parses a Ruby `Text::Line` object into a ratatui Line.
111
+ pub fn parse_line(value: Value) -> Result<Line<'static>, Error> {
112
+ let ruby = magnus::Ruby::get().unwrap();
113
+
114
+ // Get class name
115
+ let class_obj: Value = value.funcall("class", ())?;
116
+ let class_name: String = class_obj.funcall("name", ())?;
117
+
118
+ if !class_name.contains("Line") {
119
+ return Err(Error::new(
120
+ ruby.exception_type_error(),
121
+ "expected a Text::Line object",
122
+ ));
123
+ }
124
+
125
+ // Extract spans from the Ruby Line
126
+ let spans_val: Value = value.funcall("spans", ())?;
127
+
128
+ if spans_val.is_nil() {
129
+ return Ok(Line::from(""));
130
+ }
131
+
132
+ let spans_array = magnus::RArray::from_value(spans_val).ok_or_else(|| {
133
+ Error::new(
134
+ ruby.exception_type_error(),
135
+ "expected array of Spans in Text::Line.spans",
136
+ )
137
+ })?;
138
+
139
+ let mut spans = Vec::new();
140
+ for i in 0..spans_array.len() {
141
+ let ruby = magnus::Ruby::get().unwrap();
142
+ let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
143
+ let span_elem: Value = spans_array.entry(index)?;
144
+
145
+ // If it's a string, convert to span without style
146
+ if let Ok(s) = String::try_convert(span_elem) {
147
+ spans.push(Span::raw(s));
148
+ } else {
149
+ // Try to parse as Span object
150
+ if let Ok(span) = parse_span(span_elem) {
151
+ spans.push(span);
152
+ } else if let Ok(s) = String::try_convert(span_elem) {
153
+ // If it fails, try converting to string
154
+ spans.push(Span::raw(s));
155
+ }
156
+ }
157
+ }
158
+
159
+ if spans.is_empty() {
160
+ Ok(Line::from(""))
161
+ } else {
162
+ Ok(Line::from(spans))
163
+ }
164
+ }
165
+
166
+ #[cfg(test)]
167
+ mod tests {
168
+ #[test]
169
+ fn test_parse_plain_string() {
170
+ }
171
+ }
@@ -1,40 +1,101 @@
1
1
  // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
2
  // SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
- use crate::style::{parse_block, parse_style};
5
- use magnus::{prelude::*, Error, Value};
6
- use ratatui::{layout::Rect, widgets::BarChart, Frame};
4
+ use crate::style::{parse_bar_set, parse_block, parse_style};
5
+ use bumpalo::Bump;
6
+ use magnus::{prelude::*, Error, Symbol, Value, RArray};
7
+ use ratatui::{
8
+ layout::{Direction, Rect},
9
+ text::Line,
10
+ widgets::{Bar, BarChart, BarGroup},
11
+ Frame,
12
+ };
7
13
 
8
14
  pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
9
- let data_val: magnus::RHash = node.funcall("data", ())?;
15
+ let bump = Bump::new();
16
+ let data_val: Value = node.funcall("data", ())?;
10
17
  let bar_width: u16 = node.funcall("bar_width", ())?;
11
18
  let bar_gap: u16 = node.funcall("bar_gap", ())?;
19
+ let group_gap: u16 = node.funcall("group_gap", ())?;
12
20
  let max_val: Value = node.funcall("max", ())?;
13
21
  let style_val: Value = node.funcall("style", ())?;
14
22
  let block_val: Value = node.funcall("block", ())?;
23
+ let direction_sym: Symbol = node.funcall("direction", ())?;
24
+ let label_style_val: Value = node.funcall("label_style", ())?;
25
+ let value_style_val: Value = node.funcall("value_style", ())?;
26
+ let bar_set_val: Value = node.funcall("bar_set", ())?;
15
27
 
16
- let keys: magnus::RArray = data_val.funcall("keys", ())?;
17
- let mut labels = Vec::new();
18
- let mut data_vec = Vec::new();
19
-
20
- for i in 0..keys.len() {
21
- let key: Value = keys.entry(i as isize)?;
22
- let val: u64 = data_val.funcall("[]", (key,))?;
23
- let label: String = key.funcall("to_s", ())?;
24
- labels.push(label);
25
- data_vec.push(val);
26
- }
27
-
28
- let chart_data: Vec<(&str, u64)> = labels
29
- .iter()
30
- .zip(data_vec.iter())
31
- .map(|(l, v)| (l.as_str(), *v))
32
- .collect();
28
+ let direction = if direction_sym.to_string() == "horizontal" {
29
+ Direction::Horizontal
30
+ } else {
31
+ Direction::Vertical
32
+ };
33
33
 
34
34
  let mut bar_chart = BarChart::default()
35
- .data(&chart_data)
36
35
  .bar_width(bar_width)
37
- .bar_gap(bar_gap);
36
+ .bar_gap(bar_gap)
37
+ .group_gap(group_gap)
38
+ .direction(direction);
39
+
40
+ // Data parsing
41
+ let ruby = magnus::Ruby::get().unwrap();
42
+ if let Some(array) = RArray::from_value(data_val) {
43
+ for i in 0..array.len() {
44
+ let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
45
+ let group_obj: Value = array.entry(index)?;
46
+
47
+ let label_val: Value = group_obj.funcall("label", ())?;
48
+ let label_str: String = if !label_val.is_nil() {
49
+ label_val.funcall("to_s", ())?
50
+ } else {
51
+ String::new()
52
+ };
53
+ let label_ref = bump.alloc_str(&label_str) as &str;
54
+
55
+ let bars_array: RArray = group_obj.funcall("bars", ())?;
56
+ let mut bars: Vec<Bar> = Vec::new();
57
+
58
+ for j in 0..bars_array.len() {
59
+ let bar_idx = isize::try_from(j).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
60
+ let bar_obj: Value = bars_array.entry(bar_idx)?;
61
+
62
+ let value: u64 = bar_obj.funcall("value", ())?;
63
+ let mut bar = Bar::default().value(value);
64
+
65
+ let label_val: Value = bar_obj.funcall("label", ())?;
66
+ if !label_val.is_nil() {
67
+ let s: String = label_val.funcall("to_s", ())?;
68
+ let s_ref = bump.alloc_str(&s) as &str;
69
+ bar = bar.label(Line::from(s_ref));
70
+ }
71
+
72
+ let text_val: Value = bar_obj.funcall("text_value", ())?;
73
+ if !text_val.is_nil() {
74
+ let s: String = text_val.funcall("to_s", ())?;
75
+ let s_ref = bump.alloc_str(&s) as &str;
76
+ bar = bar.text_value(s_ref);
77
+ }
78
+
79
+ let style_val: Value = bar_obj.funcall("style", ())?;
80
+ if !style_val.is_nil() {
81
+ bar = bar.style(parse_style(style_val)?);
82
+ }
83
+
84
+ let val_style_val: Value = bar_obj.funcall("value_style", ())?;
85
+ if !val_style_val.is_nil() {
86
+ bar = bar.value_style(parse_style(val_style_val)?);
87
+ }
88
+
89
+ bars.push(bar);
90
+ }
91
+
92
+ let mut group = BarGroup::new(bars);
93
+ if !label_ref.is_empty() {
94
+ group = group.label(Line::from(label_ref));
95
+ }
96
+ bar_chart = bar_chart.data(group);
97
+ }
98
+ }
38
99
 
39
100
  if !max_val.is_nil() {
40
101
  let max: u64 = u64::try_convert(max_val)?;
@@ -46,7 +107,19 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
46
107
  }
47
108
 
48
109
  if !block_val.is_nil() {
49
- bar_chart = bar_chart.block(parse_block(block_val)?);
110
+ bar_chart = bar_chart.block(parse_block(block_val, &bump)?);
111
+ }
112
+
113
+ if !label_style_val.is_nil() {
114
+ bar_chart = bar_chart.label_style(parse_style(label_style_val)?);
115
+ }
116
+
117
+ if !value_style_val.is_nil() {
118
+ bar_chart = bar_chart.value_style(parse_style(value_style_val)?);
119
+ }
120
+
121
+ if !bar_set_val.is_nil() {
122
+ bar_chart = bar_chart.bar_set(parse_bar_set(bar_set_val, &bump)?);
50
123
  }
51
124
 
52
125
  frame.render_widget(bar_chart, area);
@@ -1,12 +1,40 @@
1
1
  // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
2
  // SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
+ use crate::rendering::render_node;
4
5
  use crate::style::parse_block;
5
- use magnus::{Error, Value};
6
+ use bumpalo::Bump;
7
+ use magnus::{prelude::*, Error, Value};
6
8
  use ratatui::{layout::Rect, widgets::Widget, Frame};
7
9
 
8
10
  pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
9
- let block = parse_block(node)?;
10
- block.render(area, frame.buffer_mut());
11
+ let bump = Bump::new();
12
+ let block = parse_block(node, &bump)?;
13
+ let block_clone = block.clone();
14
+
15
+ // Render the block itself (borders, styling)
16
+ block_clone.render(area, frame.buffer_mut());
17
+
18
+ // Get children and render them within the block's inner area
19
+ let children_val: Value = node.funcall("children", ())?;
20
+ let children_array = magnus::RArray::from_value(children_val);
21
+
22
+ if let Some(arr) = children_array {
23
+ if !arr.is_empty() {
24
+ // Calculate the inner area of the block (excluding borders and padding)
25
+ let inner = block.inner(area);
26
+
27
+ // Render each child in the block's inner area
28
+ for i in 0..arr.len() {
29
+ let ruby = magnus::Ruby::get().unwrap();
30
+ let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
31
+ let child: Value = arr.entry(index)?;
32
+ if let Err(e) = render_node(frame, inner, child) {
33
+ eprintln!("Error rendering block child {i}: {e:?}");
34
+ }
35
+ }
36
+ }
37
+ }
38
+
11
39
  Ok(())
12
40
  }
@@ -2,6 +2,7 @@
2
2
  // SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
4
  use crate::style::{parse_block, parse_style};
5
+ use bumpalo::Bump;
5
6
  use magnus::{prelude::*, Error, Value};
6
7
  use ratatui::{
7
8
  layout::Rect,
@@ -12,12 +13,16 @@ use std::convert::TryFrom;
12
13
  use time::{Date, Month};
13
14
 
14
15
  pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
16
+ let bump = Bump::new();
15
17
  let ruby = magnus::Ruby::get().unwrap();
16
18
  let year: i32 = node.funcall("year", ())?;
17
19
  let month_u8: u8 = node.funcall("month", ())?;
18
- let day_style_val: Value = node.funcall("day_style", ())?;
20
+ let events_val: Value = node.funcall("events", ())?;
21
+ let default_style_val: Value = node.funcall("default_style", ())?;
19
22
  let header_style_val: Value = node.funcall("header_style", ())?;
20
23
  let block_val: Value = node.funcall("block", ())?;
24
+ let show_weekdays_header: bool = node.funcall("show_weekdays_header", ())?;
25
+ let show_surrounding_val: Value = node.funcall("show_surrounding", ())?;
21
26
 
22
27
  let month = Month::try_from(month_u8)
23
28
  .map_err(|e| Error::new(ruby.exception_arg_error(), e.to_string()))?;
@@ -25,58 +30,54 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
25
30
  let date = Date::from_calendar_date(year, month, 1)
26
31
  .map_err(|e| Error::new(ruby.exception_arg_error(), e.to_string()))?;
27
32
 
28
- let mut calendar = Monthly::new(date, CalendarEventStore::default());
33
+ let mut event_store = CalendarEventStore::default();
34
+ if !events_val.is_nil() {
35
+ let events_hash = magnus::RHash::try_convert(events_val)?;
36
+ events_hash.foreach(|date_val: Value, style_val: Value| {
37
+ let year = date_val.funcall("year", ())?;
38
+ let month_u8: u8 = date_val.funcall("month", ())?;
39
+ let day: u8 = date_val.funcall("day", ())?;
29
40
 
30
- let header_style = if !header_style_val.is_nil() {
31
- parse_style(header_style_val)?
32
- } else {
41
+ let month = Month::try_from(month_u8)
42
+ .map_err(|e| Error::new(ruby.exception_arg_error(), e.to_string()))?;
43
+ let date = Date::from_calendar_date(year, month, day)
44
+ .map_err(|e| Error::new(ruby.exception_arg_error(), e.to_string()))?;
45
+ let style = parse_style(style_val)?;
46
+
47
+ event_store.add(date, style);
48
+ Ok(magnus::r_hash::ForEach::Continue)
49
+ })?;
50
+ }
51
+
52
+ let mut calendar = Monthly::new(date, event_store);
53
+
54
+ let header_style = if header_style_val.is_nil() {
33
55
  ratatui::style::Style::default()
56
+ } else {
57
+ parse_style(header_style_val)?
34
58
  };
35
- calendar = calendar
36
- .show_month_header(header_style)
37
- .show_weekdays_header(header_style);
38
59
 
39
- if !day_style_val.is_nil() {
40
- calendar = calendar.default_style(parse_style(day_style_val)?);
60
+ let show_month_header: bool = node.funcall("show_month_header", ())?;
61
+ if show_month_header {
62
+ calendar = calendar.show_month_header(header_style);
41
63
  }
42
64
 
43
- if !block_val.is_nil() {
44
- calendar = calendar.block(parse_block(block_val)?);
65
+ if show_weekdays_header {
66
+ calendar = calendar.show_weekdays_header(header_style);
45
67
  }
46
68
 
47
- frame.render_widget(calendar, area);
48
- Ok(())
49
- }
69
+ if !show_surrounding_val.is_nil() {
70
+ calendar = calendar.show_surrounding(parse_style(show_surrounding_val)?);
71
+ }
50
72
 
51
- #[cfg(test)]
52
- mod tests {
53
- use super::*;
54
- use ratatui::buffer::Buffer;
55
- use ratatui::widgets::Widget;
73
+ if !default_style_val.is_nil() {
74
+ calendar = calendar.default_style(parse_style(default_style_val)?);
75
+ }
56
76
 
57
- #[test]
58
- fn test_calendar_rendering() {
59
- let date = Date::from_calendar_date(2025, Month::December, 1).unwrap();
60
- let calendar = Monthly::new(date, CalendarEventStore::default())
61
- .show_month_header(ratatui::style::Style::default());
62
- let mut buf = Buffer::empty(Rect::new(0, 0, 40, 20));
63
- calendar.render(Rect::new(0, 0, 40, 20), &mut buf);
64
- let mut content = String::new();
65
- for y in 0..20 {
66
- for x in 0..40 {
67
- content.push_str(buf.cell((x, y)).unwrap().symbol());
68
- }
69
- content.push('\n');
70
- }
71
- assert!(
72
- content.contains("December"),
73
- "Content did not contain December: \n{}",
74
- content
75
- );
76
- assert!(
77
- content.contains("2025"),
78
- "Content did not contain 2025: \n{}",
79
- content
80
- );
77
+ if !block_val.is_nil() {
78
+ calendar = calendar.block(parse_block(block_val, &bump)?);
81
79
  }
80
+
81
+ frame.render_widget(calendar, area);
82
+ Ok(())
82
83
  }