ratatui_ruby 0.9.1 → 0.10.1

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 (268) 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 +2 -1
  7. data/CHANGELOG.md +113 -0
  8. data/README.md +17 -0
  9. data/REUSE.toml +5 -0
  10. data/Rakefile +1 -1
  11. data/Steepfile +49 -0
  12. data/doc/concepts/debugging.md +401 -0
  13. data/doc/getting_started/quickstart.md +8 -3
  14. data/doc/images/app_all_events.png +0 -0
  15. data/doc/images/app_color_picker.png +0 -0
  16. data/doc/images/app_debugging_showcase.gif +0 -0
  17. data/doc/images/app_debugging_showcase.png +0 -0
  18. data/doc/images/app_login_form.png +0 -0
  19. data/doc/images/app_stateful_interaction.png +0 -0
  20. data/doc/images/verify_quickstart_dsl.png +0 -0
  21. data/doc/images/verify_quickstart_layout.png +0 -0
  22. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  23. data/doc/images/verify_readme_usage.png +0 -0
  24. data/doc/images/widget_barchart.png +0 -0
  25. data/doc/images/widget_block.png +0 -0
  26. data/doc/images/widget_box.png +0 -0
  27. data/doc/images/widget_calendar.png +0 -0
  28. data/doc/images/widget_canvas.png +0 -0
  29. data/doc/images/widget_cell.png +0 -0
  30. data/doc/images/widget_center.png +0 -0
  31. data/doc/images/widget_chart.png +0 -0
  32. data/doc/images/widget_gauge.png +0 -0
  33. data/doc/images/widget_layout_split.png +0 -0
  34. data/doc/images/widget_line_gauge.png +0 -0
  35. data/doc/images/widget_list.png +0 -0
  36. data/doc/images/widget_map.png +0 -0
  37. data/doc/images/widget_overlay.png +0 -0
  38. data/doc/images/widget_popup.png +0 -0
  39. data/doc/images/widget_ratatui_logo.png +0 -0
  40. data/doc/images/widget_ratatui_mascot.png +0 -0
  41. data/doc/images/widget_rect.png +0 -0
  42. data/doc/images/widget_render.png +0 -0
  43. data/doc/images/widget_rich_text.png +0 -0
  44. data/doc/images/widget_scroll_text.png +0 -0
  45. data/doc/images/widget_scrollbar.png +0 -0
  46. data/doc/images/widget_sparkline.png +0 -0
  47. data/doc/images/widget_style_colors.png +0 -0
  48. data/doc/images/widget_table.png +0 -0
  49. data/doc/images/widget_tabs.png +0 -0
  50. data/doc/images/widget_text_width.png +0 -0
  51. data/doc/troubleshooting/async.md +4 -0
  52. data/examples/app_debugging_showcase/README.md +119 -0
  53. data/examples/app_debugging_showcase/app.rb +318 -0
  54. data/examples/widget_canvas/app.rb +19 -14
  55. data/examples/widget_gauge/app.rb +18 -3
  56. data/examples/widget_layout_split/app.rb +10 -4
  57. data/examples/widget_list/app.rb +22 -6
  58. data/examples/widget_rect/app.rb +7 -6
  59. data/examples/widget_rich_text/app.rb +62 -37
  60. data/examples/widget_style_colors/app.rb +26 -47
  61. data/examples/widget_table/app.rb +28 -5
  62. data/examples/widget_text_width/app.rb +6 -4
  63. data/ext/ratatui_ruby/Cargo.lock +48 -1
  64. data/ext/ratatui_ruby/Cargo.toml +6 -2
  65. data/ext/ratatui_ruby/src/color.rs +82 -0
  66. data/ext/ratatui_ruby/src/errors.rs +28 -0
  67. data/ext/ratatui_ruby/src/events.rs +15 -14
  68. data/ext/ratatui_ruby/src/lib.rs +56 -0
  69. data/ext/ratatui_ruby/src/rendering.rs +3 -1
  70. data/ext/ratatui_ruby/src/style.rs +48 -21
  71. data/ext/ratatui_ruby/src/terminal.rs +40 -9
  72. data/ext/ratatui_ruby/src/text.rs +21 -9
  73. data/ext/ratatui_ruby/src/widgets/chart.rs +2 -1
  74. data/ext/ratatui_ruby/src/widgets/layout.rs +90 -2
  75. data/ext/ratatui_ruby/src/widgets/list.rs +6 -5
  76. data/ext/ratatui_ruby/src/widgets/overlay.rs +2 -1
  77. data/ext/ratatui_ruby/src/widgets/table.rs +7 -6
  78. data/ext/ratatui_ruby/src/widgets/table_state.rs +55 -0
  79. data/ext/ratatui_ruby/src/widgets/tabs.rs +3 -2
  80. data/lib/ratatui_ruby/buffer/cell.rb +25 -15
  81. data/lib/ratatui_ruby/buffer.rb +134 -2
  82. data/lib/ratatui_ruby/cell.rb +13 -5
  83. data/lib/ratatui_ruby/debug.rb +215 -0
  84. data/lib/ratatui_ruby/event/key.rb +3 -2
  85. data/lib/ratatui_ruby/event.rb +1 -1
  86. data/lib/ratatui_ruby/layout/constraint.rb +49 -0
  87. data/lib/ratatui_ruby/layout/layout.rb +119 -13
  88. data/lib/ratatui_ruby/layout/position.rb +55 -0
  89. data/lib/ratatui_ruby/layout/rect.rb +188 -0
  90. data/lib/ratatui_ruby/layout/size.rb +55 -0
  91. data/lib/ratatui_ruby/layout.rb +4 -0
  92. data/lib/ratatui_ruby/style/color.rb +149 -0
  93. data/lib/ratatui_ruby/style/style.rb +51 -4
  94. data/lib/ratatui_ruby/style.rb +2 -0
  95. data/lib/ratatui_ruby/symbols.rb +435 -0
  96. data/lib/ratatui_ruby/synthetic_events.rb +1 -1
  97. data/lib/ratatui_ruby/table_state.rb +51 -0
  98. data/lib/ratatui_ruby/terminal_lifecycle.rb +2 -1
  99. data/lib/ratatui_ruby/test_helper/event_injection.rb +6 -1
  100. data/lib/ratatui_ruby/test_helper.rb +9 -0
  101. data/lib/ratatui_ruby/text/line.rb +245 -0
  102. data/lib/ratatui_ruby/text/span.rb +158 -0
  103. data/lib/ratatui_ruby/text.rb +99 -0
  104. data/lib/ratatui_ruby/tui/canvas_factories.rb +103 -0
  105. data/lib/ratatui_ruby/tui/core.rb +13 -2
  106. data/lib/ratatui_ruby/tui/layout_factories.rb +50 -3
  107. data/lib/ratatui_ruby/tui/state_factories.rb +42 -0
  108. data/lib/ratatui_ruby/tui/text_factories.rb +40 -0
  109. data/lib/ratatui_ruby/tui/widget_factories.rb +135 -60
  110. data/lib/ratatui_ruby/tui.rb +22 -1
  111. data/lib/ratatui_ruby/version.rb +1 -1
  112. data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +2 -0
  113. data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +2 -0
  114. data/lib/ratatui_ruby/widgets/bar_chart.rb +30 -20
  115. data/lib/ratatui_ruby/widgets/block.rb +14 -6
  116. data/lib/ratatui_ruby/widgets/calendar.rb +2 -0
  117. data/lib/ratatui_ruby/widgets/canvas.rb +56 -0
  118. data/lib/ratatui_ruby/widgets/cell.rb +2 -0
  119. data/lib/ratatui_ruby/widgets/center.rb +2 -0
  120. data/lib/ratatui_ruby/widgets/chart.rb +6 -0
  121. data/lib/ratatui_ruby/widgets/clear.rb +2 -0
  122. data/lib/ratatui_ruby/widgets/coerceable_widget.rb +77 -0
  123. data/lib/ratatui_ruby/widgets/cursor.rb +2 -0
  124. data/lib/ratatui_ruby/widgets/gauge.rb +61 -3
  125. data/lib/ratatui_ruby/widgets/line_gauge.rb +66 -4
  126. data/lib/ratatui_ruby/widgets/list.rb +87 -3
  127. data/lib/ratatui_ruby/widgets/list_item.rb +2 -0
  128. data/lib/ratatui_ruby/widgets/overlay.rb +2 -0
  129. data/lib/ratatui_ruby/widgets/paragraph.rb +4 -0
  130. data/lib/ratatui_ruby/widgets/ratatui_logo.rb +2 -0
  131. data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +2 -0
  132. data/lib/ratatui_ruby/widgets/row.rb +45 -0
  133. data/lib/ratatui_ruby/widgets/scrollbar.rb +2 -0
  134. data/lib/ratatui_ruby/widgets/shape/label.rb +2 -0
  135. data/lib/ratatui_ruby/widgets/sparkline.rb +21 -13
  136. data/lib/ratatui_ruby/widgets/table.rb +13 -3
  137. data/lib/ratatui_ruby/widgets/tabs.rb +6 -4
  138. data/lib/ratatui_ruby/widgets.rb +1 -0
  139. data/lib/ratatui_ruby.rb +42 -11
  140. data/sig/examples/app_all_events/model/app_model.rbs +23 -0
  141. data/sig/examples/app_all_events/model/event_entry.rbs +15 -8
  142. data/sig/examples/app_all_events/model/timestamp.rbs +1 -1
  143. data/sig/examples/app_all_events/view.rbs +1 -1
  144. data/sig/examples/app_stateful_interaction/app.rbs +5 -5
  145. data/sig/examples/widget_block_demo/app.rbs +6 -6
  146. data/sig/manifest.yaml +5 -0
  147. data/sig/patches/data.rbs +26 -0
  148. data/sig/patches/debugger__.rbs +8 -0
  149. data/sig/ratatui_ruby/buffer/cell.rbs +46 -0
  150. data/sig/ratatui_ruby/buffer.rbs +18 -0
  151. data/sig/ratatui_ruby/cell.rbs +44 -0
  152. data/sig/ratatui_ruby/clear.rbs +18 -0
  153. data/sig/ratatui_ruby/constraint.rbs +26 -0
  154. data/sig/ratatui_ruby/debug.rbs +45 -0
  155. data/sig/ratatui_ruby/draw.rbs +30 -0
  156. data/sig/ratatui_ruby/event.rbs +68 -8
  157. data/sig/ratatui_ruby/frame.rbs +4 -4
  158. data/sig/ratatui_ruby/interfaces.rbs +25 -0
  159. data/sig/ratatui_ruby/layout/constraint.rbs +39 -0
  160. data/sig/ratatui_ruby/layout/layout.rbs +45 -0
  161. data/sig/ratatui_ruby/layout/position.rbs +18 -0
  162. data/sig/ratatui_ruby/layout/rect.rbs +64 -0
  163. data/sig/ratatui_ruby/layout/size.rbs +18 -0
  164. data/sig/ratatui_ruby/output_guard.rbs +23 -0
  165. data/sig/ratatui_ruby/ratatui_ruby.rbs +84 -5
  166. data/sig/ratatui_ruby/rect.rbs +17 -0
  167. data/sig/ratatui_ruby/style/color.rbs +22 -0
  168. data/sig/ratatui_ruby/style/style.rbs +29 -0
  169. data/sig/ratatui_ruby/symbols.rbs +141 -0
  170. data/sig/ratatui_ruby/synthetic_events.rbs +21 -0
  171. data/sig/ratatui_ruby/table_state.rbs +6 -0
  172. data/sig/ratatui_ruby/terminal_lifecycle.rbs +31 -0
  173. data/sig/ratatui_ruby/test_helper/event_injection.rbs +2 -2
  174. data/sig/ratatui_ruby/test_helper/snapshot.rbs +22 -3
  175. data/sig/ratatui_ruby/test_helper/style_assertions.rbs +8 -1
  176. data/sig/ratatui_ruby/test_helper/test_doubles.rbs +7 -3
  177. data/sig/ratatui_ruby/text/line.rbs +27 -0
  178. data/sig/ratatui_ruby/text/span.rbs +23 -0
  179. data/sig/ratatui_ruby/text.rbs +12 -0
  180. data/sig/ratatui_ruby/tui/buffer_factories.rbs +1 -1
  181. data/sig/ratatui_ruby/tui/canvas_factories.rbs +23 -5
  182. data/sig/ratatui_ruby/tui/core.rbs +2 -2
  183. data/sig/ratatui_ruby/tui/layout_factories.rbs +16 -2
  184. data/sig/ratatui_ruby/tui/state_factories.rbs +8 -3
  185. data/sig/ratatui_ruby/tui/style_factories.rbs +3 -1
  186. data/sig/ratatui_ruby/tui/text_factories.rbs +7 -4
  187. data/sig/ratatui_ruby/tui/widget_factories.rbs +123 -30
  188. data/sig/ratatui_ruby/widgets/bar_chart.rbs +95 -0
  189. data/sig/ratatui_ruby/widgets/block.rbs +51 -0
  190. data/sig/ratatui_ruby/widgets/calendar.rbs +45 -0
  191. data/sig/ratatui_ruby/widgets/canvas.rbs +95 -0
  192. data/sig/ratatui_ruby/widgets/chart.rbs +91 -0
  193. data/sig/ratatui_ruby/widgets/coerceable_widget.rbs +26 -0
  194. data/sig/ratatui_ruby/widgets/gauge.rbs +44 -0
  195. data/sig/ratatui_ruby/widgets/line_gauge.rbs +48 -0
  196. data/sig/ratatui_ruby/widgets/list.rbs +63 -0
  197. data/sig/ratatui_ruby/widgets/misc.rbs +158 -0
  198. data/sig/ratatui_ruby/widgets/paragraph.rbs +45 -0
  199. data/sig/ratatui_ruby/widgets/row.rbs +43 -0
  200. data/sig/ratatui_ruby/widgets/scrollbar.rbs +53 -0
  201. data/sig/ratatui_ruby/widgets/shape/label.rbs +37 -0
  202. data/sig/ratatui_ruby/widgets/sparkline.rbs +45 -0
  203. data/sig/ratatui_ruby/widgets/table.rbs +78 -0
  204. data/sig/ratatui_ruby/widgets/tabs.rbs +44 -0
  205. data/sig/ratatui_ruby/{schema/list_item.rbs → widgets.rbs} +4 -4
  206. data/tasks/steep.rake +11 -0
  207. metadata +80 -63
  208. data/doc/contributors/v1.0.0_blockers.md +0 -870
  209. data/doc/troubleshooting/debugging.md +0 -101
  210. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +0 -47
  211. data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +0 -25
  212. data/lib/ratatui_ruby/schema/bar_chart.rb +0 -287
  213. data/lib/ratatui_ruby/schema/block.rb +0 -198
  214. data/lib/ratatui_ruby/schema/calendar.rb +0 -84
  215. data/lib/ratatui_ruby/schema/canvas.rb +0 -239
  216. data/lib/ratatui_ruby/schema/center.rb +0 -67
  217. data/lib/ratatui_ruby/schema/chart.rb +0 -159
  218. data/lib/ratatui_ruby/schema/clear.rb +0 -62
  219. data/lib/ratatui_ruby/schema/constraint.rb +0 -151
  220. data/lib/ratatui_ruby/schema/cursor.rb +0 -50
  221. data/lib/ratatui_ruby/schema/gauge.rb +0 -72
  222. data/lib/ratatui_ruby/schema/layout.rb +0 -122
  223. data/lib/ratatui_ruby/schema/line_gauge.rb +0 -80
  224. data/lib/ratatui_ruby/schema/list.rb +0 -135
  225. data/lib/ratatui_ruby/schema/list_item.rb +0 -51
  226. data/lib/ratatui_ruby/schema/overlay.rb +0 -51
  227. data/lib/ratatui_ruby/schema/paragraph.rb +0 -107
  228. data/lib/ratatui_ruby/schema/ratatui_logo.rb +0 -31
  229. data/lib/ratatui_ruby/schema/ratatui_mascot.rb +0 -36
  230. data/lib/ratatui_ruby/schema/rect.rb +0 -174
  231. data/lib/ratatui_ruby/schema/row.rb +0 -76
  232. data/lib/ratatui_ruby/schema/scrollbar.rb +0 -143
  233. data/lib/ratatui_ruby/schema/shape/label.rb +0 -76
  234. data/lib/ratatui_ruby/schema/sparkline.rb +0 -142
  235. data/lib/ratatui_ruby/schema/style.rb +0 -97
  236. data/lib/ratatui_ruby/schema/table.rb +0 -141
  237. data/lib/ratatui_ruby/schema/tabs.rb +0 -85
  238. data/lib/ratatui_ruby/schema/text.rb +0 -217
  239. data/sig/examples/app_all_events/model/events.rbs +0 -15
  240. data/sig/examples/app_all_events/view_state.rbs +0 -21
  241. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +0 -22
  242. data/sig/ratatui_ruby/schema/bar_chart/bar_group.rbs +0 -19
  243. data/sig/ratatui_ruby/schema/bar_chart.rbs +0 -38
  244. data/sig/ratatui_ruby/schema/block.rbs +0 -18
  245. data/sig/ratatui_ruby/schema/calendar.rbs +0 -23
  246. data/sig/ratatui_ruby/schema/canvas.rbs +0 -81
  247. data/sig/ratatui_ruby/schema/center.rbs +0 -17
  248. data/sig/ratatui_ruby/schema/chart.rbs +0 -39
  249. data/sig/ratatui_ruby/schema/constraint.rbs +0 -30
  250. data/sig/ratatui_ruby/schema/cursor.rbs +0 -16
  251. data/sig/ratatui_ruby/schema/draw.rbs +0 -33
  252. data/sig/ratatui_ruby/schema/gauge.rbs +0 -23
  253. data/sig/ratatui_ruby/schema/layout.rbs +0 -27
  254. data/sig/ratatui_ruby/schema/line_gauge.rbs +0 -24
  255. data/sig/ratatui_ruby/schema/list.rbs +0 -28
  256. data/sig/ratatui_ruby/schema/overlay.rbs +0 -15
  257. data/sig/ratatui_ruby/schema/paragraph.rbs +0 -20
  258. data/sig/ratatui_ruby/schema/ratatui_logo.rbs +0 -14
  259. data/sig/ratatui_ruby/schema/ratatui_mascot.rbs +0 -17
  260. data/sig/ratatui_ruby/schema/rect.rbs +0 -48
  261. data/sig/ratatui_ruby/schema/row.rbs +0 -28
  262. data/sig/ratatui_ruby/schema/scrollbar.rbs +0 -42
  263. data/sig/ratatui_ruby/schema/sparkline.rbs +0 -22
  264. data/sig/ratatui_ruby/schema/style.rbs +0 -19
  265. data/sig/ratatui_ruby/schema/table.rbs +0 -32
  266. data/sig/ratatui_ruby/schema/tabs.rbs +0 -21
  267. data/sig/ratatui_ruby/schema/text.rbs +0 -31
  268. /data/lib/ratatui_ruby/{schema/draw.rb → draw.rb} +0 -0
@@ -10,11 +10,13 @@ require "ratatui_ruby"
10
10
 
11
11
  # Rich Text Example
12
12
  # Demonstrates the Span and Line objects for styling individual words
13
- # within a block of text.
13
+ # within a block of text. Also demonstrates Line alignment methods.
14
14
  class WidgetRichText
15
15
  def initialize
16
16
  @scroll_pos = 0
17
17
  @color_index = 0
18
+ @alignment_index = 0
19
+ @alignments = [:left, :center, :right]
18
20
  end
19
21
 
20
22
  def run
@@ -46,40 +48,17 @@ class WidgetRichText
46
48
 
47
49
  private def simple_text_line_example
48
50
  # Example 1: A line with mixed styles
51
+ # Create a base line with spans, then apply alignment using the fluent methods
52
+ alignment = @alignments[@alignment_index]
53
+ aligned_line = case alignment
54
+ when :left then base_line.left_aligned
55
+ when :center then base_line.centered
56
+ when :right then base_line.right_aligned
57
+ end
58
+
49
59
  @tui.paragraph(
50
60
  text: [
51
- @tui.text_line(
52
- spans: [
53
- @tui.text_span(
54
- content: "Normal text, ",
55
- style: nil
56
- ),
57
- @tui.text_span(
58
- content: "Bold Text",
59
- style: @tui.style(modifiers: [:bold])
60
- ),
61
- @tui.text_span(
62
- content: ", ",
63
- style: nil
64
- ),
65
- @tui.text_span(
66
- content: "Italic Text",
67
- style: @tui.style(modifiers: [:italic])
68
- ),
69
- @tui.text_span(
70
- content: ", ",
71
- style: nil
72
- ),
73
- @tui.text_span(
74
- content: "Red Text",
75
- style: @tui.style(fg: :red)
76
- ),
77
- @tui.text_span(
78
- content: ".",
79
- style: nil
80
- ),
81
- ]
82
- ),
61
+ aligned_line,
83
62
  @tui.text_line(spans: []),
84
63
  @tui.text_line(
85
64
  spans: [
@@ -87,11 +66,18 @@ class WidgetRichText
87
66
  @tui.text_span(content: "Color #{@color_index}", style: @tui.style(fg: @color_index)),
88
67
  @tui.text_span(content: " (Use "),
89
68
  @tui.text_span(content: "↑ ↓", style: @tui.style(modifiers: [:bold])),
90
- @tui.text_span(content: " for +/- 1,", style: nil),
91
- @tui.text_span(content: "→", style: @tui.style(modifiers: [:bold])),
69
+ @tui.text_span(content: " for +/- 1, ", style: nil),
70
+ @tui.text_span(content: "→", style: @tui.style(modifiers: [:bold])),
92
71
  @tui.text_span(content: " for +/- 10)", style: nil),
93
72
  ]
94
73
  ),
74
+ @tui.text_line(spans: []),
75
+ @tui.text_line(
76
+ spans: [
77
+ @tui.text_span(content: "A", style: @tui.style(modifiers: [:bold, :underlined])),
78
+ @tui.text_span(content: ": Alignment (#{alignment})", style: nil),
79
+ ]
80
+ ),
95
81
  ],
96
82
  block: @tui.block(
97
83
  title: "Simple Rich Text",
@@ -100,8 +86,33 @@ class WidgetRichText
100
86
  )
101
87
  end
102
88
 
89
+ private def base_line
90
+ # Demonstrates creating a styled line with various modifiers and colors
91
+ # Including: underline_color (distinct from fg) and remove_modifiers (to override inherited styles)
92
+ @tui.text_line(
93
+ spans: [
94
+ @tui.text_span(content: "Normal, ", style: nil),
95
+ @tui.text_span(content: "Bold", style: @tui.style(modifiers: [:bold])),
96
+ @tui.text_span(content: ", ", style: nil),
97
+ @tui.text_span(content: "Italic", style: @tui.style(modifiers: [:italic])),
98
+ @tui.text_span(content: ", ", style: nil),
99
+ @tui.text_span(content: "Red", style: @tui.style(fg: :red)),
100
+ @tui.text_span(content: ", ", style: nil),
101
+ # New: underline_color - underline in a different color than text
102
+ @tui.text_span(
103
+ content: "Red Underline",
104
+ style: @tui.style(fg: :white, modifiers: [:underlined], underline_color: :red)
105
+ ),
106
+ @tui.text_span(content: ".", style: nil),
107
+ ]
108
+ )
109
+ end
110
+
103
111
  private def complex_example
104
112
  # Example 2: Multiple lines with different styles
113
+ # Includes Symbols::Shade constants for density gradients
114
+ shade = RatatuiRuby::Symbols::Shade
115
+
105
116
  @tui.paragraph(
106
117
  text: [
107
118
  @tui.text_line(
@@ -126,15 +137,27 @@ class WidgetRichText
126
137
  ]
127
138
  ),
128
139
  @tui.text_line(spans: []),
140
+ # Demonstrate Symbols::Shade constants for density gradients
141
+ @tui.text_line(
142
+ spans: [
143
+ @tui.text_span(content: "Shade: ", style: @tui.style(fg: :cyan)),
144
+ @tui.text_span(content: shade::EMPTY * 4, style: nil),
145
+ @tui.text_span(content: shade::LIGHT * 4, style: @tui.style(fg: :dark_gray)),
146
+ @tui.text_span(content: shade::MEDIUM * 4, style: @tui.style(fg: :gray)),
147
+ @tui.text_span(content: shade::DARK * 4, style: @tui.style(fg: :white)),
148
+ @tui.text_span(content: shade::FULL * 4, style: @tui.style(fg: :white)),
149
+ ]
150
+ ),
151
+ @tui.text_line(spans: []),
129
152
  @tui.text_line(
130
153
  spans: [
131
154
  @tui.text_span(content: "Press ", style: nil),
132
155
  @tui.text_span(content: "Q", style: @tui.style(modifiers: [:bold])),
133
156
  @tui.text_span(content: " to quit, ", style: nil),
134
157
  @tui.text_span(content: "↑ ↓", style: @tui.style(modifiers: [:bold])),
135
- @tui.text_span(content: " to adjust color by 1, ", style: nil),
158
+ @tui.text_span(content: ": color by 1, ", style: nil),
136
159
  @tui.text_span(content: "← →", style: @tui.style(modifiers: [:bold])),
137
- @tui.text_span(content: " to adjust color by 10.", style: nil),
160
+ @tui.text_span(content: ": color by 10.", style: nil),
138
161
  ]
139
162
  ),
140
163
  ],
@@ -157,6 +180,8 @@ class WidgetRichText
157
180
  @color_index = (@color_index + 1) % 256
158
181
  elsif event.down?
159
182
  @color_index = (@color_index - 1) % 256
183
+ elsif event == "a"
184
+ @alignment_index = (@alignment_index + 1) % @alignments.size
160
185
  end
161
186
 
162
187
  nil
@@ -8,6 +8,15 @@
8
8
  $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
9
9
  require "ratatui_ruby"
10
10
 
11
+ # Demonstrates terminal color rendering and the Color module.
12
+ #
13
+ # Terminal apps need vibrant colors. Specifying colors as symbols or
14
+ # calculated hex strings works but limits expressiveness.
15
+ #
16
+ # This demo shows the Color module's constructors for creating colors
17
+ # from HSL values or hex integers, producing a full-spectrum gradient.
18
+ #
19
+ # Use it to understand color representation and the Color.hsl/Color.hex APIs.
11
20
  class WidgetStyleColors
12
21
  def initialize
13
22
  @width = 80
@@ -35,12 +44,19 @@ class WidgetStyleColors
35
44
  hue = (col.to_f / @width) * 360.0
36
45
  lightness = 50.0 - ((row.to_f / @height) * 50.0)
37
46
 
38
- rgb = hsl_to_rgb(hue, 100.0, lightness)
39
- hex = rgb_to_hex(rgb)
47
+ # Use Color.hsl for top half, Color.hsluv for bottom half
48
+ # HSLuv provides perceptually uniform colors (same visual brightness)
49
+ hex = if row < @height / 2
50
+ RatatuiRuby::Style::Color.hsl(hue, 100.0, lightness)
51
+ else
52
+ # HSLuv: perceptually uniform - all colors appear equal brightness
53
+ RatatuiRuby::Style::Color.hsluv(hue, 100.0, lightness)
54
+ end
40
55
 
56
+ # Demonstrate Style.with for concise inline styling
41
57
  span = tui.text_span(
42
58
  content: " ",
43
- style: tui.style(bg: hex)
59
+ style: RatatuiRuby::Style::Style.with(bg: hex)
44
60
  )
45
61
  spans << span
46
62
  end
@@ -48,57 +64,20 @@ class WidgetStyleColors
48
64
  lines << tui.text_line(spans:)
49
65
  end
50
66
 
67
+ # Also demonstrate Color.hex for the border
68
+ border_color = RatatuiRuby::Style::Color.hex(0xFFD700) # Gold
69
+
51
70
  tui.paragraph(
52
71
  text: lines,
53
72
  block: tui.block(
54
- title: "Hex Color Gradient (Press 'q' or Ctrl+C to exit)",
73
+ title: "HSL (top) vs HSLuv (bottom) - Style.with demo (Press 'q' to exit)",
55
74
  borders: [:all],
56
- border_type: :rounded
75
+ border_type: :rounded,
76
+ # Using Style.with for concise border styling
77
+ border_style: RatatuiRuby::Style::Style.with(fg: border_color)
57
78
  )
58
79
  )
59
80
  end
60
-
61
- private def hsl_to_rgb(hue, saturation, lightness)
62
- h = hue / 360.0
63
- s = saturation / 100.0
64
- l = lightness / 100.0
65
-
66
- if s == 0
67
- r = g = b = l
68
- else
69
- q = (l < 0.5) ? l * (1 + s) : l + s - (l * s)
70
- p = (2 * l) - q
71
-
72
- r = hue_to_rgb(p, q, h + (1.0 / 3.0))
73
- g = hue_to_rgb(p, q, h)
74
- b = hue_to_rgb(p, q, h - (1.0 / 3.0))
75
- end
76
-
77
- [
78
- (r * 255).round,
79
- (g * 255).round,
80
- (b * 255).round,
81
- ]
82
- end
83
-
84
- private def hue_to_rgb(p, q, t)
85
- t += 1 while t < 0
86
- t -= 1 while t > 1
87
-
88
- if t < 1.0 / 6.0
89
- p + ((q - p) * 6 * t)
90
- elsif t < 1.0 / 2.0
91
- q
92
- elsif t < 2.0 / 3.0
93
- p + ((q - p) * ((2.0 / 3.0) - t) * 6)
94
- else
95
- p
96
- end
97
- end
98
-
99
- private def rgb_to_hex(rgb)
100
- "##{rgb.map { |c| c.to_s(16).upcase.rjust(2, '0') }.join}"
101
- end
102
81
  end
103
82
 
104
83
  WidgetStyleColors.new.run if __FILE__ == $PROGRAM_NAME
@@ -55,6 +55,7 @@ class WidgetTable
55
55
  @show_cell_highlight = true
56
56
  @offset_mode_index = 0
57
57
  @flex_mode_index = 0
58
+ @strikethrough_pids = Set.new # Track which rows have strikethrough
58
59
  end
59
60
 
60
61
  def run
@@ -78,28 +79,38 @@ class WidgetTable
78
79
  { name: "Blue on White", style: @tui.style(fg: :blue, bg: :white) },
79
80
  { name: "Magenta", style: @tui.style(fg: :magenta, modifiers: [:bold]) },
80
81
  ]
81
- @column_highlight_style = @tui.style(fg: :magenta)
82
+ @column_highlight_style = @tui.style(fg: :red)
82
83
  @cell_highlight_style = @tui.style(fg: :white, bg: :red, modifiers: [:bold])
83
84
  @hotkey_style = @tui.style(modifiers: [:bold, :underlined])
84
85
  end
85
86
 
86
87
  private def render(frame)
87
88
  # v0.7.0: Create table rows using table_row and table_cell for per-cell styling
88
- rows = PROCESSES.map do |p|
89
+ rows = PROCESSES.each_with_index.map do |p, i|
89
90
  cpu_style = case p[:cpu]
90
91
  when 0...10 then @tui.style(fg: :green)
91
92
  when 10...30 then @tui.style(fg: :yellow)
92
93
  else @tui.style(fg: :red, modifiers: [:bold])
93
94
  end
94
- @tui.table_row(
95
+ row = @tui.table_row(
95
96
  cells: [
96
97
  p[:pid].to_s,
97
98
  p[:name],
98
99
  @tui.table_cell(content: "#{p[:cpu]}%", style: cpu_style),
99
100
  ],
100
- # Apply alternating row backgrounds for readability
101
- style: p[:pid].even? ? @tui.style(bg: :dark_gray) : nil
101
+ # Apply alternating row backgrounds for readability (using basic ANSI colors for compatibility)
102
+ style: i.even? ? @tui.style(bg: :white, fg: :black) : nil
102
103
  )
104
+
105
+ # Row#enable_strikethrough: Apply strikethrough to "tamped" (de-emphasized) processes.
106
+ # Note: Strikethrough (SGR 9) is not supported by all terminals. macOS Terminal.app
107
+ # notably lacks support, while Kitty, iTerm2, Alacritty, and WezTerm render it.
108
+ # We add :dim as a fallback so the effect is visible even without strikethrough.
109
+ if @strikethrough_pids.include?(p[:pid])
110
+ row.enable_strikethrough.with(style: (row.style || @tui.style).with(modifiers: ((row.style&.modifiers || []) + [:crossed_out, :dim]).uniq))
111
+ else
112
+ row
113
+ end
103
114
  end
104
115
 
105
116
  # Define column widths
@@ -170,6 +181,8 @@ class WidgetTable
170
181
  @tui.text_span(content: ": Style (#{current_style_entry[:name]}) "),
171
182
  @tui.text_span(content: "p", style: @hotkey_style),
172
183
  @tui.text_span(content: ": Spacing (#{current_spacing_entry[:name]}) "),
184
+ @tui.text_span(content: "t", style: @hotkey_style),
185
+ @tui.text_span(content: ": Tamp Row"),
173
186
  ]),
174
187
  # Line 3: More Controls
175
188
  @tui.text_line(spans: [
@@ -237,6 +250,16 @@ class WidgetTable
237
250
  @highlight_spacing_index = (@highlight_spacing_index + 1) % HIGHLIGHT_SPACINGS.length
238
251
  in type: :key, code: "x"
239
252
  @selected_index = @selected_index.nil? ? 0 : nil
253
+ in type: :key, code: "t"
254
+ # Toggle strikethrough for selected row (demonstrates Row#enable_strikethrough)
255
+ if @selected_index
256
+ pid = PROCESSES[@selected_index][:pid]
257
+ if @strikethrough_pids.include?(pid)
258
+ @strikethrough_pids.delete(pid)
259
+ else
260
+ @strikethrough_pids.add(pid)
261
+ end
262
+ end
240
263
  in type: :key, code: "c"
241
264
  @show_column_highlight = !@show_column_highlight
242
265
  in type: :key, code: "z"
@@ -51,10 +51,11 @@ class WidgetTextWidth
51
51
  sample = @text_samples[@selected_index]
52
52
  measured_width = @tui.text_width(sample[:text])
53
53
 
54
- # v0.7.0: Text::Line#width instance method for rich text measurement
55
- styled_line = @tui.text_line(spans: [
56
- @tui.text_span(content: sample[:text], style: @tui.style(fg: :cyan)),
57
- ])
54
+ # v0.7.0: Text::Span#width and Text::Line#width instance methods for rich text measurement
55
+ styled_span = @tui.text_span(content: sample[:text], style: @tui.style(fg: :cyan))
56
+ span_width = styled_span.width
57
+
58
+ styled_line = @tui.text_line(spans: [styled_span])
58
59
  line_width = styled_line.width
59
60
 
60
61
  # Build content text with newlines
@@ -62,6 +63,7 @@ class WidgetTextWidth
62
63
  content << "Sample: #{sample[:text]}"
63
64
  content << ""
64
65
  content << "Display Width (text_width): #{measured_width} cells"
66
+ content << "Display Width (span.width): #{span_width} cells"
65
67
  content << "Display Width (line.width): #{line_width} cells"
66
68
  content << "Character Count: #{sample[:text].length}"
67
69
  content << ""
@@ -23,6 +23,15 @@ version = "1.0.100"
23
23
  source = "registry+https://github.com/rust-lang/crates.io-index"
24
24
  checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
25
25
 
26
+ [[package]]
27
+ name = "approx"
28
+ version = "0.5.1"
29
+ source = "registry+https://github.com/rust-lang/crates.io-index"
30
+ checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
31
+ dependencies = [
32
+ "num-traits",
33
+ ]
34
+
26
35
  [[package]]
27
36
  name = "atomic"
28
37
  version = "0.6.1"
@@ -106,6 +115,12 @@ version = "3.19.1"
106
115
  source = "registry+https://github.com/rust-lang/crates.io-index"
107
116
  checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
108
117
 
118
+ [[package]]
119
+ name = "by_address"
120
+ version = "1.2.1"
121
+ source = "registry+https://github.com/rust-lang/crates.io-index"
122
+ checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06"
123
+
109
124
  [[package]]
110
125
  name = "bytemuck"
111
126
  version = "1.24.0"
@@ -364,6 +379,12 @@ dependencies = [
364
379
  "regex",
365
380
  ]
366
381
 
382
+ [[package]]
383
+ name = "fast-srgb8"
384
+ version = "1.0.0"
385
+ source = "registry+https://github.com/rust-lang/crates.io-index"
386
+ checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
387
+
367
388
  [[package]]
368
389
  name = "filedescriptor"
369
390
  version = "0.8.3"
@@ -756,6 +777,30 @@ dependencies = [
756
777
  "num-traits",
757
778
  ]
758
779
 
780
+ [[package]]
781
+ name = "palette"
782
+ version = "0.7.6"
783
+ source = "registry+https://github.com/rust-lang/crates.io-index"
784
+ checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6"
785
+ dependencies = [
786
+ "approx",
787
+ "fast-srgb8",
788
+ "palette_derive",
789
+ "phf",
790
+ ]
791
+
792
+ [[package]]
793
+ name = "palette_derive"
794
+ version = "0.7.6"
795
+ source = "registry+https://github.com/rust-lang/crates.io-index"
796
+ checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30"
797
+ dependencies = [
798
+ "by_address",
799
+ "proc-macro2",
800
+ "quote",
801
+ "syn 2.0.111",
802
+ ]
803
+
759
804
  [[package]]
760
805
  name = "parking_lot"
761
806
  version = "0.12.5"
@@ -932,6 +977,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
932
977
  checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc"
933
978
  dependencies = [
934
979
  "instability",
980
+ "palette",
935
981
  "ratatui-core",
936
982
  "ratatui-crossterm",
937
983
  "ratatui-macros",
@@ -952,6 +998,7 @@ dependencies = [
952
998
  "itertools 0.14.0",
953
999
  "kasuari",
954
1000
  "lru",
1001
+ "palette",
955
1002
  "strum",
956
1003
  "thiserror 2.0.17",
957
1004
  "unicode-segmentation",
@@ -1012,7 +1059,7 @@ dependencies = [
1012
1059
 
1013
1060
  [[package]]
1014
1061
  name = "ratatui_ruby"
1015
- version = "0.9.1"
1062
+ version = "0.10.1"
1016
1063
  dependencies = [
1017
1064
  "bumpalo",
1018
1065
  "lazy_static",
@@ -3,7 +3,7 @@
3
3
 
4
4
  [package]
5
5
  name = "ratatui_ruby"
6
- version = "0.9.1"
6
+ version = "0.10.1"
7
7
  edition = "2021"
8
8
 
9
9
  [lib]
@@ -11,9 +11,13 @@ crate-type = ["cdylib", "staticlib"]
11
11
 
12
12
  [dependencies]
13
13
  magnus = "0.8.2"
14
- ratatui = { version = "0.30", features = ["widget-calendar", "layout-cache", "unstable-rendered-line-info"] }
14
+ ratatui = { version = "0.30", features = ["widget-calendar", "layout-cache", "unstable-rendered-line-info", "palette"] }
15
15
  unicode-width = "0.1"
16
16
 
17
17
  bumpalo = "3.16"
18
18
  lazy_static = "1.4"
19
19
  time = { version = "0.3", features = ["macros"] }
20
+
21
+ [profile.release]
22
+ debug = true
23
+ strip = false
@@ -0,0 +1,82 @@
1
+ // SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ //! Color conversion functions exposed to Ruby.
5
+ //!
6
+ //! These functions wrap Ratatui's color conversion methods that require the `palette` feature.
7
+
8
+ use magnus::{Error, Ruby};
9
+ use ratatui::palette::{Hsl, Hsluv};
10
+ use ratatui::style::Color;
11
+
12
+ /// Formats an RGB color as a hex string.
13
+ fn rgb_to_hex(red: u8, green: u8, blue: u8) -> String {
14
+ format!("#{red:02x}{green:02x}{blue:02x}")
15
+ }
16
+
17
+ /// Convert HSL values to a hex color string.
18
+ ///
19
+ /// # Arguments
20
+ /// * `hue` - Hue in degrees (0-360, wraps)
21
+ /// * `saturation` - Saturation as percentage (0-100)
22
+ /// * `lightness` - Lightness as percentage (0-100)
23
+ ///
24
+ /// # Returns
25
+ /// A hex color string like "#rrggbb"
26
+ pub fn from_hsl(_ruby: &Ruby, hue: f32, saturation: f32, lightness: f32) -> String {
27
+ // Normalize: h wraps, s and l are percentages (0-100) that need to be 0-1
28
+ let hsl = Hsl::new(hue, saturation / 100.0, lightness / 100.0);
29
+ let color = Color::from_hsl(hsl);
30
+
31
+ match color {
32
+ Color::Rgb(red, green, blue) => rgb_to_hex(red, green, blue),
33
+ _ => "#000000".to_string(),
34
+ }
35
+ }
36
+
37
+ /// Convert `HSLuv` values to a hex color string.
38
+ ///
39
+ /// `HSLuv` is a perceptually uniform color space where colors at the same
40
+ /// lightness appear equally bright regardless of hue.
41
+ ///
42
+ /// # Arguments
43
+ /// * `hue` - Hue in degrees (-180 to 360, wraps)
44
+ /// * `saturation` - Saturation as percentage (0-100)
45
+ /// * `lightness` - Lightness as percentage (0-100)
46
+ ///
47
+ /// # Returns
48
+ /// A hex color string like "#rrggbb"
49
+ pub fn from_hsluv(_ruby: &Ruby, hue: f32, saturation: f32, lightness: f32) -> String {
50
+ // Hsluv expects h in degrees, s and l as percentages (0-100)
51
+ let hsluv = Hsluv::new(hue, saturation, lightness);
52
+ let color = Color::from_hsluv(hsluv);
53
+
54
+ match color {
55
+ Color::Rgb(red, green, blue) => rgb_to_hex(red, green, blue),
56
+ _ => "#000000".to_string(),
57
+ }
58
+ }
59
+
60
+ /// Convert a u32 to a hex color string.
61
+ ///
62
+ /// # Arguments
63
+ /// * `val` - A u32 in the format 0x00RRGGBB
64
+ ///
65
+ /// # Returns
66
+ /// A hex color string like "#rrggbb"
67
+ pub fn from_u32(_ruby: &Ruby, val: u32) -> String {
68
+ let color = Color::from_u32(val);
69
+
70
+ match color {
71
+ Color::Rgb(red, green, blue) => rgb_to_hex(red, green, blue),
72
+ _ => "#000000".to_string(),
73
+ }
74
+ }
75
+
76
+ /// Register color module functions in the `RatatuiRuby` module.
77
+ pub fn register(_ruby: &Ruby, module: magnus::RModule) -> Result<(), Error> {
78
+ module.define_module_function("_color_from_hsl", magnus::function!(from_hsl, 3))?;
79
+ module.define_module_function("_color_from_hsluv", magnus::function!(from_hsluv, 3))?;
80
+ module.define_module_function("_color_from_u32", magnus::function!(from_u32, 1))?;
81
+ Ok(())
82
+ }
@@ -0,0 +1,28 @@
1
+ // SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ use magnus::{prelude::*, Error, Value};
5
+
6
+ /// Creates a `TypeError` with context showing the actual value received.
7
+ ///
8
+ /// Calls `inspect` on the Ruby value and includes it in the error message.
9
+ /// Long inspect strings (>200 chars) are truncated.
10
+ ///
11
+ /// # Example error message
12
+ /// ```text
13
+ /// expected array for rows, got {:title=>"Processes", :header=>["Name", ...]}
14
+ /// ```
15
+ pub fn type_error_with_context(ruby: &magnus::Ruby, expected: &str, got: Value) -> Error {
16
+ let inspect: String = got
17
+ .funcall("inspect", ())
18
+ .unwrap_or_else(|_| "<uninspectable>".to_string());
19
+ let truncated = if inspect.len() > 200 {
20
+ format!("{}...", &inspect[..200])
21
+ } else {
22
+ inspect
23
+ };
24
+ Error::new(
25
+ ruby.exception_type_error(),
26
+ format!("{expected}, got {truncated}"),
27
+ )
28
+ }
@@ -30,25 +30,26 @@ pub fn inject_test_event(event_type: String, data: magnus::RHash) -> Result<(),
30
30
 
31
31
  /// Parses a `snake_case` string to `MediaKeyCode`.
32
32
  ///
33
- /// Accepts both the new `media_`-prefixed codes (canonical) and the legacy
34
- /// unprefixed codes for backward compatibility with existing tests.
33
+ /// Parses a `snake_case` string to `MediaKeyCode`.
34
+ ///
35
+ /// Only accepts the `media_`-prefixed codes (canonical). Legacy unprefixed codes are no longer supported.
35
36
  fn parse_media_key(s: &str) -> Option<ratatui::crossterm::event::MediaKeyCode> {
36
37
  use ratatui::crossterm::event::MediaKeyCode;
37
38
  match s {
38
39
  // New canonical codes (media_ prefix)
39
- "media_play" | "play" => Some(MediaKeyCode::Play),
40
+ "media_play" => Some(MediaKeyCode::Play),
40
41
  "media_pause" => Some(MediaKeyCode::Pause),
41
- "media_play_pause" | "play_pause" => Some(MediaKeyCode::PlayPause),
42
- "media_reverse" | "reverse" => Some(MediaKeyCode::Reverse),
43
- "media_stop" | "stop" => Some(MediaKeyCode::Stop),
44
- "media_fast_forward" | "fast_forward" => Some(MediaKeyCode::FastForward),
45
- "media_rewind" | "rewind" => Some(MediaKeyCode::Rewind),
46
- "media_track_next" | "track_next" => Some(MediaKeyCode::TrackNext),
47
- "media_track_previous" | "track_previous" => Some(MediaKeyCode::TrackPrevious),
48
- "media_record" | "record" => Some(MediaKeyCode::Record),
49
- "media_lower_volume" | "lower_volume" => Some(MediaKeyCode::LowerVolume),
50
- "media_raise_volume" | "raise_volume" => Some(MediaKeyCode::RaiseVolume),
51
- "media_mute_volume" | "mute_volume" => Some(MediaKeyCode::MuteVolume),
42
+ "media_play_pause" => Some(MediaKeyCode::PlayPause),
43
+ "media_reverse" => Some(MediaKeyCode::Reverse),
44
+ "media_stop" => Some(MediaKeyCode::Stop),
45
+ "media_fast_forward" => Some(MediaKeyCode::FastForward),
46
+ "media_rewind" => Some(MediaKeyCode::Rewind),
47
+ "media_track_next" => Some(MediaKeyCode::TrackNext),
48
+ "media_track_previous" => Some(MediaKeyCode::TrackPrevious),
49
+ "media_record" => Some(MediaKeyCode::Record),
50
+ "media_lower_volume" => Some(MediaKeyCode::LowerVolume),
51
+ "media_raise_volume" => Some(MediaKeyCode::RaiseVolume),
52
+ "media_mute_volume" => Some(MediaKeyCode::MuteVolume),
52
53
  _ => None,
53
54
  }
54
55
  }