ratatui_ruby 1.0.0 → 1.1.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 (88) 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 +3 -2
  7. data/CHANGELOG.md +33 -7
  8. data/Steepfile +1 -0
  9. data/doc/concepts/application_testing.md +5 -5
  10. data/doc/concepts/event_handling.md +1 -1
  11. data/doc/contributors/design/ruby_frontend.md +40 -12
  12. data/doc/contributors/design/rust_backend.md +13 -1
  13. data/doc/contributors/releasing.md +215 -0
  14. data/doc/contributors/todo/align/api_completeness_audit-finished.md +6 -0
  15. data/doc/contributors/todo/align/api_completeness_audit-unfinished.md +1 -7
  16. data/doc/contributors/todo/align/term.md +351 -0
  17. data/doc/contributors/upstream_requests/paragraph_span_rects.md +259 -0
  18. data/doc/getting_started/quickstart.md +1 -1
  19. data/doc/getting_started/why.md +3 -3
  20. data/doc/images/app_external_editor.gif +0 -0
  21. data/doc/index.md +1 -6
  22. data/examples/app_external_editor/README.md +62 -0
  23. data/examples/app_external_editor/app.rb +344 -0
  24. data/examples/widget_list/app.rb +2 -4
  25. data/examples/widget_table/app.rb +8 -2
  26. data/ext/ratatui_ruby/Cargo.lock +1 -1
  27. data/ext/ratatui_ruby/Cargo.toml +1 -1
  28. data/ext/ratatui_ruby/src/events.rs +171 -203
  29. data/ext/ratatui_ruby/src/lib.rs +36 -0
  30. data/ext/ratatui_ruby/src/lib_header.rs +11 -0
  31. data/ext/ratatui_ruby/src/terminal/capabilities.rs +46 -0
  32. data/ext/ratatui_ruby/src/terminal/init.rs +92 -0
  33. data/ext/ratatui_ruby/src/terminal/mod.rs +12 -3
  34. data/ext/ratatui_ruby/src/terminal/queries.rs +15 -0
  35. data/ext/ratatui_ruby/src/terminal/query.rs +64 -2
  36. data/lib/ratatui_ruby/backend/window_size.rb +50 -0
  37. data/lib/ratatui_ruby/backend.rb +59 -0
  38. data/lib/ratatui_ruby/event/key/navigation.rb +10 -1
  39. data/lib/ratatui_ruby/event/key.rb +84 -0
  40. data/lib/ratatui_ruby/event/mouse.rb +95 -3
  41. data/lib/ratatui_ruby/event/resize.rb +45 -3
  42. data/lib/ratatui_ruby/layout/alignment.rb +91 -0
  43. data/lib/ratatui_ruby/layout/layout.rb +1 -2
  44. data/lib/ratatui_ruby/layout/size.rb +10 -3
  45. data/lib/ratatui_ruby/layout.rb +4 -0
  46. data/lib/ratatui_ruby/terminal/capabilities.rb +316 -0
  47. data/lib/ratatui_ruby/terminal/viewport.rb +1 -1
  48. data/lib/ratatui_ruby/terminal.rb +66 -0
  49. data/lib/ratatui_ruby/test_helper/global_state.rb +111 -0
  50. data/lib/ratatui_ruby/test_helper.rb +3 -0
  51. data/lib/ratatui_ruby/version.rb +1 -1
  52. data/lib/ratatui_ruby/widgets/table.rb +2 -2
  53. data/lib/ratatui_ruby.rb +25 -4
  54. data/sig/examples/app_external_editor/app.rbs +12 -0
  55. data/sig/generated/event_key_predicates.rbs +1348 -0
  56. data/sig/ratatui_ruby/backend/window_size.rbs +17 -0
  57. data/sig/ratatui_ruby/backend.rbs +12 -0
  58. data/sig/ratatui_ruby/event.rbs +7 -0
  59. data/sig/ratatui_ruby/layout/alignment.rbs +26 -0
  60. data/sig/ratatui_ruby/ratatui_ruby.rbs +2 -0
  61. data/sig/ratatui_ruby/terminal/capabilities.rbs +38 -0
  62. data/sig/ratatui_ruby/terminal/viewport.rbs +15 -1
  63. data/tasks/bump/bump_workflow.rb +49 -0
  64. data/tasks/bump/changelog.rb +57 -0
  65. data/tasks/bump/patch_release.rb +19 -0
  66. data/tasks/bump/release_branch.rb +17 -0
  67. data/tasks/bump/release_from_trunk.rb +49 -0
  68. data/tasks/bump/repository.rb +54 -0
  69. data/tasks/bump/ruby_gem.rb +6 -26
  70. data/tasks/bump/sem_ver.rb +4 -0
  71. data/tasks/bump/unreleased_section.rb +17 -0
  72. data/tasks/bump.rake +21 -11
  73. data/tasks/doc/documentation.rb +59 -0
  74. data/tasks/doc/link/file_url.rb +30 -0
  75. data/tasks/doc/link/relative_path.rb +61 -0
  76. data/tasks/doc/link/web_url.rb +55 -0
  77. data/tasks/doc/link.rb +52 -0
  78. data/tasks/doc/link_audit.rb +116 -0
  79. data/tasks/doc/problem.rb +40 -0
  80. data/tasks/doc/source_file.rb +93 -0
  81. data/tasks/doc.rake +18 -0
  82. data/tasks/rbs_predicates/predicate_catalog.rb +52 -0
  83. data/tasks/rbs_predicates/predicate_tests.rb +124 -0
  84. data/tasks/rbs_predicates/rbs_signature.rb +63 -0
  85. data/tasks/rbs_predicates.rake +31 -0
  86. data/tasks/test.rake +3 -0
  87. data/tasks/website/version.rb +23 -28
  88. metadata +38 -1
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: LGPL-3.0-or-later
6
+ #++
7
+
8
+ module RatatuiRuby
9
+ module Layout
10
+ # Horizontal content alignment within a layout area.
11
+ #
12
+ # Use these constants for discoverability, or pass symbols directly
13
+ # (<tt>:left</tt>, <tt>:center</tt>, <tt>:right</tt>).
14
+ #
15
+ # Mirrors +ratatui::layout::HorizontalAlignment+ from upstream Ratatui.
16
+ #
17
+ # === Example
18
+ #
19
+ #--
20
+ # SPDX-SnippetBegin
21
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
22
+ # SPDX-License-Identifier: MIT-0
23
+ #++
24
+ # # Using constants (discoverable)
25
+ # paragraph = Paragraph.new(
26
+ # text: "Hello",
27
+ # alignment: HorizontalAlignment::CENTER
28
+ # )
29
+ #
30
+ # # Using symbols directly (idiomatic Ruby)
31
+ # paragraph = Paragraph.new(text: "Hello", alignment: :center)
32
+ #--
33
+ # SPDX-SnippetEnd
34
+ #++
35
+ module HorizontalAlignment
36
+ # Align content to the left edge.
37
+ LEFT = :left
38
+
39
+ # Align content to the center.
40
+ CENTER = :center
41
+
42
+ # Align content to the right edge.
43
+ RIGHT = :right
44
+
45
+ # All valid alignment values.
46
+ ALL = [LEFT, CENTER, RIGHT].freeze
47
+ end
48
+
49
+ # Vertical content alignment within a layout area.
50
+ #
51
+ # Use these constants for discoverability, or pass symbols directly
52
+ # (<tt>:top</tt>, <tt>:center</tt>, <tt>:bottom</tt>).
53
+ #
54
+ # Mirrors +ratatui::layout::VerticalAlignment+ from upstream Ratatui.
55
+ #
56
+ # === Example
57
+ #
58
+ #--
59
+ # SPDX-SnippetBegin
60
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
61
+ # SPDX-License-Identifier: MIT-0
62
+ #++
63
+ # # Using constants (discoverable)
64
+ # widget.vertical_alignment = VerticalAlignment::CENTER
65
+ #
66
+ # # Using symbols directly (idiomatic Ruby)
67
+ # widget.vertical_alignment = :center
68
+ #--
69
+ # SPDX-SnippetEnd
70
+ #++
71
+ module VerticalAlignment
72
+ # Align content to the top edge.
73
+ TOP = :top
74
+
75
+ # Align content to the center.
76
+ CENTER = :center
77
+
78
+ # Align content to the bottom edge.
79
+ BOTTOM = :bottom
80
+
81
+ # All valid alignment values.
82
+ ALL = [TOP, CENTER, BOTTOM].freeze
83
+ end
84
+
85
+ # Type alias for HorizontalAlignment.
86
+ #
87
+ # Provided for upstream API parity. In Ratatui, +Alignment+ was the
88
+ # original name before +HorizontalAlignment+ was introduced in v0.30.0.
89
+ Alignment = HorizontalAlignment
90
+ end
91
+ end
@@ -49,8 +49,7 @@ module RatatuiRuby
49
49
  #
50
50
  # One of <tt>:legacy</tt>, <tt>:start</tt>, <tt>:center</tt>, <tt>:end</tt>, <tt>:space_between</tt>, <tt>:space_around</tt>.
51
51
 
52
- # :nodoc:
53
- FLEX_MODES = %i[legacy start center end space_between space_around space_evenly].freeze
52
+ FLEX_MODES = %i[legacy start center end space_between space_around space_evenly].freeze # :nodoc:
54
53
 
55
54
  ##
56
55
  # Direction: split vertically (top to bottom).
@@ -7,7 +7,7 @@
7
7
 
8
8
  module RatatuiRuby
9
9
  module Layout
10
- # Terminal dimensions as width and height.
10
+ # Generic dimensions as width and height.
11
11
  #
12
12
  # Layout calculations need sizes. Passing width and height
13
13
  # as separate arguments is verbose and easy to swap by mistake.
@@ -15,6 +15,13 @@ module RatatuiRuby
15
15
  # This class bundles dimensions into a single immutable object.
16
16
  # Extract it from a Rect or create it directly for sizing operations.
17
17
  #
18
+ # Following upstream Ratatui's design, the same Size type represents
19
+ # both character-grid dimensions (columns × rows) and pixel dimensions.
20
+ # The semantic meaning depends on the source:
21
+ #
22
+ # - From <tt>Terminal.size</tt> or <tt>WindowSize#columns_rows</tt>: columns and rows
23
+ # - From <tt>WindowSize#pixels</tt>: pixel width and height
24
+ #
18
25
  # Use it for terminal dimensions, widget sizing constraints,
19
26
  # or anywhere you need width/height without position.
20
27
  #
@@ -37,11 +44,11 @@ module RatatuiRuby
37
44
  class Size < Data.define(:width, :height)
38
45
  ##
39
46
  # :attr_reader: width
40
- # Width in terminal columns.
47
+ # Width dimension (columns for character grids, pixels for pixel sizes).
41
48
 
42
49
  ##
43
50
  # :attr_reader: height
44
- # Height in terminal rows.
51
+ # Height dimension (rows for character grids, pixels for pixel sizes).
45
52
 
46
53
  # Creates a new Size.
47
54
  #
@@ -14,10 +14,14 @@ module RatatuiRuby
14
14
  # - {Size} — Terminal dimensions
15
15
  # - {Constraint} — Sizing rules
16
16
  # - {Layout} — Space distribution
17
+ # - {HorizontalAlignment} — Horizontal alignment constants
18
+ # - {VerticalAlignment} — Vertical alignment constants
19
+ # - {Alignment} — Alias for HorizontalAlignment
17
20
  module Layout
18
21
  end
19
22
  end
20
23
 
24
+ require_relative "layout/alignment"
21
25
  require_relative "layout/rect"
22
26
  require_relative "layout/position"
23
27
  require_relative "layout/size"
@@ -0,0 +1,316 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: LGPL-3.0-or-later
6
+ #++
7
+
8
+ require "timeout"
9
+
10
+ module RatatuiRuby
11
+ class Terminal
12
+ # Environment-based terminal capability detection.
13
+ #
14
+ # TUI applications need to know what the terminal supports. Color depth
15
+ # varies. Some terminals lack escape sequence support entirely. Users
16
+ # set environment variables like <tt>NO_COLOR</tt> to express preferences.
17
+ #
18
+ # This module detects terminal capabilities from environment variables.
19
+ # It checks <tt>TERM</tt>, <tt>COLORTERM</tt>, <tt>NO_COLOR</tt>, and
20
+ # <tt>FORCE_COLOR</tt> to determine what the terminal supports.
21
+ #
22
+ # Use these methods before initializing a Terminal instance to decide
23
+ # whether TUI mode is appropriate for the current environment.
24
+ #
25
+ # === Example
26
+ #
27
+ #--
28
+ # SPDX-SnippetBegin
29
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
30
+ # SPDX-License-Identifier: MIT-0
31
+ #++
32
+ # if RatatuiRuby::Terminal.interactive?
33
+ # RatatuiRuby.run { |tui| ... }
34
+ # else
35
+ # puts "TUI mode not available"
36
+ # end
37
+ #--
38
+ # SPDX-SnippetEnd
39
+ #++
40
+ module Capabilities
41
+ # Checks if stdout connects to a terminal device.
42
+ #
43
+ # Terminal apps render escape sequences. Piped output or log files
44
+ # cannot interpret them. If your app writes ANSI codes to a non-TTY,
45
+ # logs fill with garbage like <tt>[32m</tt> instead of green text.
46
+ #
47
+ # This method checks <tt>$stdout.tty?</tt>. Use it to skip TUI mode
48
+ # when output is redirected. Print plain text instead.
49
+ #
50
+ # === Example
51
+ #
52
+ #--
53
+ # SPDX-SnippetBegin
54
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
55
+ # SPDX-License-Identifier: MIT-0
56
+ #++
57
+ # if RatatuiRuby::Terminal.tty?
58
+ # start_tui
59
+ # else
60
+ # print_plain_output
61
+ # end
62
+ #--
63
+ # SPDX-SnippetEnd
64
+ #++
65
+ def tty?
66
+ $stdout.tty?
67
+ end
68
+
69
+ # Checks if the terminal declared itself "dumb."
70
+ #
71
+ # Dumb terminals exist. Emacs shell-mode sets <tt>TERM=dumb</tt>.
72
+ # Serial consoles do too. These terminals cannot interpret escape
73
+ # sequences. If your app sends cursor movements or colors, output
74
+ # becomes unreadable.
75
+ #
76
+ # This method checks for explicit <tt>TERM=dumb</tt>. Empty or unset
77
+ # <tt>TERM</tt> means "unknown," not "dumb." Use it to fall back to
78
+ # plain text rendering.
79
+ #
80
+ # === Example
81
+ #
82
+ #--
83
+ # SPDX-SnippetBegin
84
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
85
+ # SPDX-License-Identifier: MIT-0
86
+ #++
87
+ # if RatatuiRuby::Terminal.dumb?
88
+ # render_plain_table(data)
89
+ # else
90
+ # render_styled_table(data)
91
+ # end
92
+ #--
93
+ # SPDX-SnippetEnd
94
+ #++
95
+ def dumb?
96
+ ENV["TERM"] == "dumb"
97
+ end
98
+
99
+ # Checks if the user disabled color output.
100
+ #
101
+ # Users with visual impairments or screen readers often disable
102
+ # colors. The NO_COLOR standard (no-color.org) provides a universal
103
+ # way to request this. Ignoring it frustrates accessibility-conscious
104
+ # users.
105
+ #
106
+ # This method checks for <tt>NO_COLOR</tt> in the environment. The
107
+ # value does not matter; presence alone disables color. Respect it.
108
+ #
109
+ # === Example
110
+ #
111
+ #--
112
+ # SPDX-SnippetBegin
113
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
114
+ # SPDX-License-Identifier: MIT-0
115
+ #++
116
+ # style = RatatuiRuby::Terminal.no_color? ? :plain : :colored
117
+ #--
118
+ # SPDX-SnippetEnd
119
+ #++
120
+ def no_color?
121
+ ENV.key?("NO_COLOR")
122
+ end
123
+
124
+ # Checks if color output is explicitly forced.
125
+ #
126
+ # Some CI systems and logging pipelines detect non-TTY and strip
127
+ # colors. Users want colors anyway for readability. <tt>FORCE_COLOR</tt>
128
+ # overrides the TTY check.
129
+ #
130
+ # This method checks for <tt>FORCE_COLOR</tt> in the environment.
131
+ # When set, your app should emit colors even when <tt>tty?</tt>
132
+ # returns false.
133
+ #
134
+ # === Example
135
+ #
136
+ #--
137
+ # SPDX-SnippetBegin
138
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
139
+ # SPDX-License-Identifier: MIT-0
140
+ #++
141
+ # use_color = RatatuiRuby::Terminal.tty? ||
142
+ # RatatuiRuby::Terminal.force_color?
143
+ #--
144
+ # SPDX-SnippetEnd
145
+ #++
146
+ def force_color?
147
+ ENV.key?("FORCE_COLOR")
148
+ end
149
+
150
+ # Checks if the terminal supports interactive TUI mode.
151
+ #
152
+ # A TUI needs a real terminal. Piped output breaks cursor control.
153
+ # Dumb terminals corrupt escape sequences. Starting TUI mode in
154
+ # these environments wastes resources and confuses users.
155
+ #
156
+ # This method combines <tt>tty?</tt> and <tt>dumb?</tt> checks.
157
+ # Returns +true+ only when both conditions allow TUI operation.
158
+ # Use it as the gatekeeper before calling <tt>run</tt>.
159
+ #
160
+ # === Example
161
+ #
162
+ #--
163
+ # SPDX-SnippetBegin
164
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
165
+ # SPDX-License-Identifier: MIT-0
166
+ #++
167
+ # if RatatuiRuby::Terminal.interactive?
168
+ # RatatuiRuby.run { |tui| main_loop(tui) }
169
+ # else
170
+ # abort "Interactive terminal required"
171
+ # end
172
+ #--
173
+ # SPDX-SnippetEnd
174
+ #++
175
+ def interactive?
176
+ tty? && !dumb?
177
+ end
178
+
179
+ # Queries how many colors the terminal can display.
180
+ #
181
+ # Modern terminals vary wildly in capability. Some only support 8 ANSI
182
+ # colors. Others display 256. High-end terminals render 16 million
183
+ # truecolor shades. If your app uses rich color palettes without
184
+ # checking, users on basic terminals see garbled output or crashes.
185
+ #
186
+ # This method queries crossterm (which checks <tt>COLORTERM</tt> and
187
+ # <tt>TERM</tt>) and returns the raw count. Use it to select color
188
+ # palettes or degrade gracefully.
189
+ #
190
+ # Returns 8, 256, or 65535 (truecolor).
191
+ #
192
+ # === Example
193
+ #
194
+ #--
195
+ # SPDX-SnippetBegin
196
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
197
+ # SPDX-License-Identifier: MIT-0
198
+ #++
199
+ # colors = RatatuiRuby::Terminal.available_color_count
200
+ # palette = colors >= 256 ? :rich : :basic
201
+ #--
202
+ # SPDX-SnippetEnd
203
+ #++
204
+ def available_color_count
205
+ _available_color_count
206
+ end
207
+
208
+ # Returns the terminal's color capability as a symbol.
209
+ #
210
+ # Comparing integers is annoying. You want to know: can I use
211
+ # gradients? Do I need a fallback palette? This method translates
212
+ # the raw count into semantic symbols.
213
+ #
214
+ # Returns <tt>:none</tt> when <tt>NO_COLOR</tt> is set or terminal is
215
+ # dumb. Returns <tt>:basic</tt> (8 colors), <tt>:ansi256</tt> (256),
216
+ # or <tt>:truecolor</tt> (16M) based on capability.
217
+ #
218
+ # Use it to switch rendering strategies or skip color entirely.
219
+ #
220
+ # === Example
221
+ #
222
+ #--
223
+ # SPDX-SnippetBegin
224
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
225
+ # SPDX-License-Identifier: MIT-0
226
+ #++
227
+ # case RatatuiRuby::Terminal.color_support
228
+ # when :truecolor then use_gradient_theme
229
+ # when :ansi256 then use_256_palette
230
+ # when :basic then use_ansi_colors
231
+ # when :none then use_monochrome
232
+ # end
233
+ #--
234
+ # SPDX-SnippetEnd
235
+ #++
236
+ def color_support
237
+ return :none if no_color?
238
+ return :none if dumb?
239
+
240
+ count = available_color_count
241
+ return :truecolor if count >= 65_535
242
+ return :ansi256 if count >= 256
243
+
244
+ :basic
245
+ end
246
+
247
+ # Checks for Kitty keyboard protocol support.
248
+ #
249
+ # Standard terminal input is ambiguous. Escape key and arrow keys
250
+ # share prefixes. Modifier keys get lost. Applications that need
251
+ # precise key handling (editors, games) struggle with the limitations.
252
+ #
253
+ # The Kitty keyboard protocol solves this. Terminals that support it
254
+ # report key presses unambiguously, with full modifier information.
255
+ # This method queries support so you can enable enhanced input or
256
+ # fall back gracefully.
257
+ #
258
+ # Returns <tt>false</tt> immediately if <tt>tty?</tt> returns false.
259
+ # Otherwise queries crossterm with a 0.5s timeout.
260
+ # Returns <tt>true</tt> only if the terminal responds affirmatively.
261
+ #
262
+ # === Example
263
+ #
264
+ #--
265
+ # SPDX-SnippetBegin
266
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
267
+ # SPDX-License-Identifier: MIT-0
268
+ #++
269
+ # if RatatuiRuby::Terminal.supports_keyboard_enhancement?
270
+ # enable_vim_style_keybindings
271
+ # else
272
+ # use_simple_navigation
273
+ # end
274
+ #--
275
+ # SPDX-SnippetEnd
276
+ #++
277
+ def supports_keyboard_enhancement?
278
+ return false unless tty?
279
+
280
+ Timeout.timeout(0.5) { _supports_keyboard_enhancement }
281
+ rescue
282
+ false
283
+ end
284
+
285
+ # Globally override NO_COLOR detection.
286
+ #
287
+ # The NO_COLOR environment variable tells applications to disable color.
288
+ # Sometimes users want colors anyway, like in CI pipelines that log to
289
+ # files but display logs with color. Passing +true+ forces color output
290
+ # regardless of NO_COLOR.
291
+ #
292
+ # This method calls crossterm's global override. The effect is immediate
293
+ # and affects all subsequent color detection queries. Pass +false+ to
294
+ # restore normal NO_COLOR behavior.
295
+ #
296
+ # === Example
297
+ #
298
+ #--
299
+ # SPDX-SnippetBegin
300
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
301
+ # SPDX-License-Identifier: MIT-0
302
+ #++
303
+ # if options[:color] == :always
304
+ # RatatuiRuby::Terminal.force_color_output(true)
305
+ # end
306
+ #--
307
+ # SPDX-SnippetEnd
308
+ #++
309
+ def force_color_output(enable)
310
+ _force_color_output(enable)
311
+ end
312
+ end
313
+
314
+ extend Capabilities
315
+ end
316
+ end
@@ -17,7 +17,7 @@ module RatatuiRuby
17
17
  # This module handles the choice. It defines viewport modes and their parameters.
18
18
  #
19
19
  # @see Terminal::Viewport
20
- module Terminal
20
+ class Terminal
21
21
  ##
22
22
  # Viewport configuration for terminal initialization.
23
23
  #
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: LGPL-3.0-or-later
6
+ #++
7
+
8
+ require_relative "terminal/capabilities"
9
+
10
+ module RatatuiRuby
11
+ # Terminal object for managing terminal lifecycle and rendering.
12
+ #
13
+ # Instance-based API aligned with upstream Ratatui Terminal struct.
14
+ #
15
+ # === Example
16
+ #
17
+ #--
18
+ # SPDX-SnippetBegin
19
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
20
+ # SPDX-License-Identifier: MIT-0
21
+ #++
22
+ # terminal = RatatuiRuby::Terminal.new
23
+ # terminal.draw { |frame| ... }
24
+ # terminal.restore
25
+ #--
26
+ # SPDX-SnippetEnd
27
+ #++
28
+ class Terminal
29
+ ##
30
+ # :attr_reader: terminal_id
31
+ # Unique identifier for this terminal instance in Rust (Integer).
32
+ attr_reader :terminal_id
33
+
34
+ # Creates a new Terminal instance.
35
+ #
36
+ # [viewport] Symbol or Viewport object (:fullscreen or :inline)
37
+ # [height] Integer height for inline viewports
38
+ def initialize(viewport: :fullscreen, height: nil)
39
+ @viewport = resolve_viewport(viewport, height)
40
+
41
+ # Call Rust FFI to create instance and get ID
42
+ # For now, only test backend is supported (real crossterm coming later)
43
+ @terminal_id = self.class._init_test_terminal_instance(
44
+ 80, # default width for test
45
+ 24, # default height for test
46
+ @viewport.type.to_s,
47
+ @viewport.height
48
+ )
49
+ end
50
+
51
+ # Returns the terminal size as a Layout::Rect
52
+ # Rust constructs the Rect object directly (not a hash!)
53
+ def size
54
+ self.class._get_terminal_size_instance(@terminal_id)
55
+ end
56
+
57
+ private def resolve_viewport(viewport, height)
58
+ case viewport
59
+ when nil, :fullscreen then Terminal::Viewport.fullscreen
60
+ when :inline then Terminal::Viewport.inline(height || 8)
61
+ when Terminal::Viewport then viewport
62
+ else raise ArgumentError, "Unknown viewport: #{viewport.inspect}"
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long &lt;me@kerricklong.com&gt;
5
+ # SPDX-License-Identifier: LGPL-3.0-or-later
6
+ #++
7
+
8
+ module RatatuiRuby
9
+ module TestHelper
10
+ ##
11
+ # Helpers for testing code that reads global state.
12
+ #
13
+ # Applications often read +ARGV+ and +ENV+ at startup. Testing these code
14
+ # paths requires stubbing those constants. Doing it manually is brittle —
15
+ # tests that forget to restore the original values leak state.
16
+ #
17
+ # These helpers swap the constants, yield to the block, and restore the
18
+ # originals automatically.
19
+ #
20
+ # == Example
21
+ #
22
+ #--
23
+ # SPDX-SnippetBegin
24
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
25
+ # SPDX-License-Identifier: MIT-0
26
+ #++
27
+ # def test_parses_command_line_flags
28
+ # with_argv(["--verbose", "--log=debug.log"]) do
29
+ # app = MyApp.new
30
+ # assert app.verbose?
31
+ # assert_equal "debug.log", app.log_path
32
+ # end
33
+ # end
34
+ #
35
+ # def test_reads_api_key_from_environment
36
+ # with_env("API_KEY", "test-key-1234") do
37
+ # client = APIClient.new
38
+ # assert_equal "test-key-1234", client.api_key
39
+ # end
40
+ # end
41
+ #--
42
+ # SPDX-SnippetEnd
43
+ #++
44
+ module GlobalState
45
+ # Temporarily replaces ARGV for the duration of the block.
46
+ #
47
+ # [argv] An array of strings to use as ARGV.
48
+ #
49
+ # === Example
50
+ #
51
+ #--
52
+ # SPDX-SnippetBegin
53
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
54
+ # SPDX-License-Identifier: MIT-0
55
+ #++
56
+ # with_argv(["--help"]) do
57
+ # # Code here sees ARGV as ["--help"]
58
+ # end
59
+ #--
60
+ # SPDX-SnippetEnd
61
+ #++
62
+ def with_argv(argv)
63
+ original_argv = ARGV.dup
64
+ ARGV.replace(argv)
65
+ yield
66
+ ensure
67
+ ARGV.replace(original_argv)
68
+ end
69
+
70
+ # Temporarily replaces an environment variable for the duration of the block.
71
+ #
72
+ # If +value+ is +nil+, the variable is deleted for the block's duration.
73
+ #
74
+ # [key] The environment variable name (String).
75
+ # [value] The value to set, or +nil+ to delete.
76
+ #
77
+ # === Example
78
+ #
79
+ #--
80
+ # SPDX-SnippetBegin
81
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
82
+ # SPDX-License-Identifier: MIT-0
83
+ #++
84
+ # with_env("DEBUG", "1") do
85
+ # # Code here sees ENV["DEBUG"] as "1"
86
+ # end
87
+ #
88
+ # with_env("DEBUG", nil) do
89
+ # # Code here does not see ENV["DEBUG"]
90
+ # end
91
+ #--
92
+ # SPDX-SnippetEnd
93
+ #++
94
+ def with_env(key, value)
95
+ original_value = ENV[key]
96
+ if value.nil?
97
+ ENV.delete(key)
98
+ else
99
+ ENV[key] = value
100
+ end
101
+ yield
102
+ ensure
103
+ if original_value.nil?
104
+ ENV.delete(key)
105
+ else
106
+ ENV[key] = original_value
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -11,6 +11,7 @@ require_relative "test_helper/snapshot"
11
11
  require_relative "test_helper/event_injection"
12
12
  require_relative "test_helper/style_assertions"
13
13
  require_relative "test_helper/test_doubles"
14
+ require_relative "test_helper/global_state"
14
15
 
15
16
  module RatatuiRuby
16
17
  ##
@@ -30,6 +31,7 @@ module RatatuiRuby
30
31
  # [EventInjection] Simulates keypresses, mouse clicks, and resize events.
31
32
  # [StyleAssertions] Checks foreground color, background color, and text modifiers.
32
33
  # [TestDoubles] Provides mocks and stubs for testing views in isolation.
34
+ # [GlobalState] Provides with_argv and with_env helpers for testing global state access.
33
35
  #
34
36
  # == Example
35
37
  #
@@ -106,5 +108,6 @@ module RatatuiRuby
106
108
  include EventInjection
107
109
  include StyleAssertions
108
110
  include TestDoubles
111
+ include GlobalState
109
112
  end
110
113
  end
@@ -8,5 +8,5 @@
8
8
  module RatatuiRuby
9
9
  # The version of the ratatui_ruby gem.
10
10
  # See https://semver.org/spec/v2.0.0.html
11
- VERSION = "1.0.0"
11
+ VERSION = "1.1.0"
12
12
  end