ratatui_ruby 0.10.1 → 0.10.2
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.
- checksums.yaml +4 -4
- data/.builds/ruby-3.2.yml +1 -1
- data/.builds/ruby-3.3.yml +1 -1
- data/.builds/ruby-3.4.yml +1 -1
- data/.builds/ruby-4.0.0.yml +1 -1
- data/CHANGELOG.md +24 -0
- data/doc/concepts/application_architecture.md +2 -2
- data/doc/concepts/application_testing.md +1 -1
- data/doc/concepts/custom_widgets.md +2 -2
- data/doc/contributors/todo/align/api_completeness_audit-finished.md +375 -0
- data/doc/contributors/todo/align/api_completeness_audit-unfinished.md +206 -0
- data/doc/contributors/todo/align/terminal.md +647 -0
- data/doc/getting_started/quickstart.md +41 -41
- data/doc/images/app_cli_rich_moments.gif +0 -0
- data/examples/app_cli_rich_moments/README.md +81 -0
- data/examples/app_cli_rich_moments/app.rb +189 -0
- data/ext/ratatui_ruby/Cargo.lock +1 -1
- data/ext/ratatui_ruby/Cargo.toml +1 -1
- data/ext/ratatui_ruby/src/frame.rs +17 -4
- data/ext/ratatui_ruby/src/lib.rs +17 -3
- data/ext/ratatui_ruby/src/lib.rs.bak +286 -0
- data/ext/ratatui_ruby/src/rendering.rs +38 -25
- data/ext/ratatui_ruby/src/rendering.rs.bak +152 -0
- data/ext/ratatui_ruby/src/terminal.rs +245 -33
- data/ext/ratatui_ruby/src/terminal.rs.bak +381 -0
- data/ext/ratatui_ruby/src/terminal.rs.orig +409 -0
- data/ext/ratatui_ruby/src/widgets/barchart.rs +4 -3
- data/ext/ratatui_ruby/src/widgets/block.rs +4 -4
- data/ext/ratatui_ruby/src/widgets/calendar.rs +4 -3
- data/ext/ratatui_ruby/src/widgets/canvas.rs +7 -4
- data/ext/ratatui_ruby/src/widgets/center.rs +3 -3
- data/ext/ratatui_ruby/src/widgets/chart.rs +4 -4
- data/ext/ratatui_ruby/src/widgets/clear.rs +6 -6
- data/ext/ratatui_ruby/src/widgets/cursor.rs +10 -7
- data/ext/ratatui_ruby/src/widgets/gauge.rs +4 -3
- data/ext/ratatui_ruby/src/widgets/layout.rs +3 -3
- data/ext/ratatui_ruby/src/widgets/line_gauge.rs +4 -3
- data/ext/ratatui_ruby/src/widgets/list.rs +6 -9
- data/ext/ratatui_ruby/src/widgets/overlay.rs +3 -3
- data/ext/ratatui_ruby/src/widgets/paragraph.rs +5 -6
- data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +4 -4
- data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +8 -4
- data/ext/ratatui_ruby/src/widgets/scrollbar.rs +10 -10
- data/ext/ratatui_ruby/src/widgets/sparkline.rs +4 -3
- data/ext/ratatui_ruby/src/widgets/table.rs +6 -6
- data/ext/ratatui_ruby/src/widgets/tabs.rs +4 -3
- data/lib/ratatui_ruby/labs/a11y.rb +173 -0
- data/lib/ratatui_ruby/labs/frame_a11y_capture.rb +50 -0
- data/lib/ratatui_ruby/labs.rb +47 -0
- data/lib/ratatui_ruby/layout/position.rb +26 -0
- data/lib/ratatui_ruby/terminal/viewport.rb +80 -0
- data/lib/ratatui_ruby/terminal_lifecycle.rb +164 -6
- data/lib/ratatui_ruby/terminal_lifecycle.rb.bak +197 -0
- data/lib/ratatui_ruby/test_helper/terminal.rb +8 -1
- data/lib/ratatui_ruby/tui/core.rb +16 -0
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby.rb +82 -3
- data/migrate_to_buffer.rb +145 -0
- data/sig/examples/app_cli_rich_moments/app.rbs +12 -0
- data/sig/ratatui_ruby/labs.rbs +87 -0
- data/sig/ratatui_ruby/ratatui_ruby.rbs +12 -4
- data/sig/ratatui_ruby/terminal/viewport.rbs +19 -0
- data/sig/ratatui_ruby/terminal_lifecycle.rbs +13 -5
- data/sig/ratatui_ruby/tui/core.rbs +3 -0
- metadata +21 -2
- /data/doc/contributors/{future_work.md → todo/future_work.md} +0 -0
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
##
|
|
10
|
+
# Terminal configuration and viewport settings.
|
|
11
|
+
#
|
|
12
|
+
# Your app needs to choose how it occupies the terminal.
|
|
13
|
+
# Fullscreen apps take over the whole screen and clear on exit.
|
|
14
|
+
# Inline apps run in a fixed region and persist in scrollback.
|
|
15
|
+
# Configuring this manually is error-prone.
|
|
16
|
+
#
|
|
17
|
+
# This module handles the choice. It defines viewport modes and their parameters.
|
|
18
|
+
#
|
|
19
|
+
# @see Terminal::Viewport
|
|
20
|
+
module Terminal
|
|
21
|
+
##
|
|
22
|
+
# Viewport configuration for terminal initialization.
|
|
23
|
+
#
|
|
24
|
+
# Determines how RatatuiRuby interacts with the terminal:
|
|
25
|
+
# - **Fullscreen**: Uses alternate screen, clears on exit (default)
|
|
26
|
+
# - **Inline**: Fixed-height region, persists in scrollback after exit
|
|
27
|
+
#
|
|
28
|
+
# === Example
|
|
29
|
+
#
|
|
30
|
+
#--
|
|
31
|
+
# SPDX-SnippetBegin
|
|
32
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
33
|
+
# SPDX-License-Identifier: MIT-0
|
|
34
|
+
#++
|
|
35
|
+
# # Fullscreen (default behavior)
|
|
36
|
+
# RatatuiRuby.run { |tui| ... }
|
|
37
|
+
#
|
|
38
|
+
# # Inline with 8 lines
|
|
39
|
+
# RatatuiRuby.run(viewport: :inline, height: 8) { |tui| ... }
|
|
40
|
+
#--
|
|
41
|
+
# SPDX-SnippetEnd
|
|
42
|
+
#++
|
|
43
|
+
class Viewport < Data.define(:type, :height)
|
|
44
|
+
##
|
|
45
|
+
# Creates a fullscreen viewport (alternate screen).
|
|
46
|
+
def self.fullscreen
|
|
47
|
+
new(type: :fullscreen)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# Creates an inline viewport with the given height.
|
|
52
|
+
def self.inline(height)
|
|
53
|
+
new(type: :inline, height:)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
# Creates a new viewport configuration.
|
|
58
|
+
#
|
|
59
|
+
# [type] Symbol representing viewport type (:fullscreen or :inline).
|
|
60
|
+
# [height] Integer height in lines (required for :inline, ignored for :fullscreen).
|
|
61
|
+
#
|
|
62
|
+
# Most developers use {.fullscreen} or {.inline} factory methods instead.
|
|
63
|
+
def initialize(type:, height: nil)
|
|
64
|
+
super
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
##
|
|
68
|
+
# Returns true if this is a fullscreen viewport.
|
|
69
|
+
def fullscreen?
|
|
70
|
+
type == :fullscreen
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
##
|
|
74
|
+
# Returns true if this is an inline viewport.
|
|
75
|
+
def inline?
|
|
76
|
+
type == :inline
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -57,15 +57,24 @@ module RatatuiRuby
|
|
|
57
57
|
#
|
|
58
58
|
# @raise [Error::Invariant] if headless mode is enabled or a session is already active
|
|
59
59
|
# @see headless!
|
|
60
|
-
def init_terminal(focus_events: true, bracketed_paste: true)
|
|
60
|
+
def init_terminal(focus_events: true, bracketed_paste: true, viewport: nil, height: nil)
|
|
61
61
|
if @headless_mode
|
|
62
62
|
raise Error::Invariant, "Cannot initialize terminal: headless mode is enabled"
|
|
63
63
|
end
|
|
64
64
|
if @tui_session_active
|
|
65
65
|
raise Error::Invariant, "Cannot initialize terminal: TUI session already active"
|
|
66
66
|
end
|
|
67
|
+
|
|
68
|
+
# Show A11Y lab prompt before launching TUI (stdout visible now, not after)
|
|
69
|
+
if Labs.enabled?(:a11y)
|
|
70
|
+
puts Labs::A11y.startup_message
|
|
71
|
+
$stdin.gets
|
|
72
|
+
end
|
|
73
|
+
|
|
67
74
|
@tui_session_active = true
|
|
68
|
-
|
|
75
|
+
|
|
76
|
+
viewport_obj = resolve_viewport(viewport, height)
|
|
77
|
+
_init_terminal(focus_events, bracketed_paste, viewport_obj.type.to_s, viewport_obj.height)
|
|
69
78
|
end
|
|
70
79
|
|
|
71
80
|
##
|
|
@@ -76,7 +85,7 @@ module RatatuiRuby
|
|
|
76
85
|
# [height] Integer height of the test terminal.
|
|
77
86
|
#
|
|
78
87
|
# @raise [Error::Invariant] if headless mode is enabled or a session is already active
|
|
79
|
-
def init_test_terminal(width, height)
|
|
88
|
+
def init_test_terminal(width, height, viewport_type = "fullscreen", viewport_height = nil)
|
|
80
89
|
if @headless_mode
|
|
81
90
|
raise Error::Invariant, "Cannot initialize terminal: headless mode is enabled"
|
|
82
91
|
end
|
|
@@ -84,7 +93,7 @@ module RatatuiRuby
|
|
|
84
93
|
raise Error::Invariant, "Cannot initialize terminal: TUI session already active"
|
|
85
94
|
end
|
|
86
95
|
@tui_session_active = true
|
|
87
|
-
_init_test_terminal(width, height)
|
|
96
|
+
_init_test_terminal(width, height, viewport_type, viewport_height)
|
|
88
97
|
end
|
|
89
98
|
|
|
90
99
|
##
|
|
@@ -135,11 +144,160 @@ module RatatuiRuby
|
|
|
135
144
|
#++
|
|
136
145
|
# @raise [Error::Invariant] if headless mode is enabled
|
|
137
146
|
# @see headless!
|
|
138
|
-
def run(focus_events: true, bracketed_paste: true)
|
|
139
|
-
init_terminal(focus_events:, bracketed_paste:)
|
|
147
|
+
def run(focus_events: true, bracketed_paste: true, viewport: nil, height: nil)
|
|
148
|
+
init_terminal(focus_events:, bracketed_paste:, viewport:, height:)
|
|
140
149
|
yield TUI.new
|
|
141
150
|
ensure
|
|
142
151
|
restore_terminal
|
|
143
152
|
end
|
|
153
|
+
|
|
154
|
+
##
|
|
155
|
+
# Inserts content above an inline viewport into scrollback.
|
|
156
|
+
#
|
|
157
|
+
# Your inline TUI runs at the bottom of the terminal. Users scroll back
|
|
158
|
+
# to see earlier CLI output. You want to add new content to that scrollback
|
|
159
|
+
# without disrupting the running TUI viewport.
|
|
160
|
+
#
|
|
161
|
+
# Calling <tt>draw</tt> only updates the viewport itself. Content drawn there
|
|
162
|
+
# disappears into scrollback when the viewport moves. You have no way to insert
|
|
163
|
+
# historical content directly into the scrollback buffer above your running TUI.
|
|
164
|
+
#
|
|
165
|
+
# This method inserts content above the viewport. The viewport stays live and interactive.
|
|
166
|
+
# New content appears above it in scrollback history. If the viewport isn't yet at the
|
|
167
|
+
# bottom of the screen, it shifts down. Once at the bottom, inserted content scrolls
|
|
168
|
+
# the scrollback buffer upward.
|
|
169
|
+
#
|
|
170
|
+
# Use it to log status updates, progress messages, or diagnostic output while your
|
|
171
|
+
# inline TUI continues running.
|
|
172
|
+
#
|
|
173
|
+
# === Behavior
|
|
174
|
+
#
|
|
175
|
+
# The method renders a widget into a temporary buffer of <tt>height</tt> lines,
|
|
176
|
+
# then inserts that buffer above the viewport. The viewport may shift position
|
|
177
|
+
# on screen to accommodate the new lines.
|
|
178
|
+
#
|
|
179
|
+
# When the viewport has room to move down (not yet at screen bottom):
|
|
180
|
+
# - Inserted lines appear above the viewport
|
|
181
|
+
# - The viewport shifts down by <tt>height</tt> lines
|
|
182
|
+
# - The viewport's visual position changes
|
|
183
|
+
#
|
|
184
|
+
# When the viewport is already at the screen bottom:
|
|
185
|
+
# - Inserted lines push existing scrollback upward
|
|
186
|
+
# - The viewport stays at the bottom
|
|
187
|
+
# - The viewport's content may be disturbed by scrolling
|
|
188
|
+
#
|
|
189
|
+
# After calling this method, you should call <tt>draw</tt> to redraw the viewport's
|
|
190
|
+
# content over its new position. The viewport area may have been clobbered by
|
|
191
|
+
# the insertion or scrolling.
|
|
192
|
+
#
|
|
193
|
+
# This method has no effect when called on a fullscreen viewport. Fullscreen
|
|
194
|
+
# viewports have no scrollback buffer.
|
|
195
|
+
#
|
|
196
|
+
# [height] Number of lines to insert (Integer). Must be positive.
|
|
197
|
+
# [widget] Widget to render into the inserted buffer, or <tt>nil</tt> if using block.
|
|
198
|
+
# [block] Optional block that returns a widget to render.
|
|
199
|
+
#
|
|
200
|
+
# === Examples
|
|
201
|
+
#
|
|
202
|
+
# ==== Insert a status line while TUI runs
|
|
203
|
+
#
|
|
204
|
+
#--
|
|
205
|
+
# SPDX-SnippetBegin
|
|
206
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
207
|
+
# SPDX-License-Identifier: MIT-0
|
|
208
|
+
#++
|
|
209
|
+
# RatatuiRuby.run(viewport: :inline, height: 3) do |tui|
|
|
210
|
+
# loop do
|
|
211
|
+
# tui.draw { |f| f.render_widget(tui.paragraph(text: "TUI Running"), f.area) }
|
|
212
|
+
#
|
|
213
|
+
# event = tui.poll_event
|
|
214
|
+
# if event[:type] == :key && event[:code] == "l"
|
|
215
|
+
# # Insert a log line above the running TUI
|
|
216
|
+
# tui.insert_before(1, tui.paragraph(text: "[#{Time.now}] User pressed 'l'"))
|
|
217
|
+
# # Redraw viewport to restore its content
|
|
218
|
+
# tui.draw { |f| f.render_widget(tui.paragraph(text: "TUI Running"), f.area) }
|
|
219
|
+
# end
|
|
220
|
+
# end
|
|
221
|
+
# end
|
|
222
|
+
#--
|
|
223
|
+
# SPDX-SnippetEnd
|
|
224
|
+
#++
|
|
225
|
+
#
|
|
226
|
+
# @raise [Error::Invariant] if the current viewport is not inline
|
|
227
|
+
def insert_before(height, widget = nil, &block)
|
|
228
|
+
# Validate we're in an inline viewport
|
|
229
|
+
viewport_type = RatatuiRuby._get_viewport_type
|
|
230
|
+
unless viewport_type == "inline"
|
|
231
|
+
raise Error::Invariant, "insert_before requires an inline viewport"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
content = widget || block&.call
|
|
235
|
+
_insert_before(height, content)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
private def resolve_viewport(viewport, height)
|
|
239
|
+
case viewport
|
|
240
|
+
when nil, :fullscreen then Terminal::Viewport.fullscreen
|
|
241
|
+
when :inline then Terminal::Viewport.inline(height || 8)
|
|
242
|
+
when Terminal::Viewport then viewport
|
|
243
|
+
else raise ArgumentError, "Unknown viewport: #{viewport.inspect}"
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Sets the cursor position using a Position object or coordinates.
|
|
248
|
+
#
|
|
249
|
+
# This is a convenience alias for +set_cursor_position+ that works with
|
|
250
|
+
# Position objects or raw arrays rather than separate x/y arguments.
|
|
251
|
+
#
|
|
252
|
+
# [position] A Layout::Position, Array of [x, y], or anything that responds to +x+ and +y+.
|
|
253
|
+
#
|
|
254
|
+
# === Examples
|
|
255
|
+
#
|
|
256
|
+
#--
|
|
257
|
+
# SPDX-SnippetBegin
|
|
258
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
259
|
+
# SPDX-License-Identifier: MIT-0
|
|
260
|
+
#++
|
|
261
|
+
# # With Position object
|
|
262
|
+
# pos = RatatuiRuby::Layout::Position.new(x: 10, y: 5)
|
|
263
|
+
# RatatuiRuby.cursor_position = pos
|
|
264
|
+
#
|
|
265
|
+
# # With array shorthand
|
|
266
|
+
# RatatuiRuby.cursor_position = 10, 5
|
|
267
|
+
#--
|
|
268
|
+
# SPDX-SnippetEnd
|
|
269
|
+
#++
|
|
270
|
+
def cursor_position=(position)
|
|
271
|
+
if position.is_a?(Array)
|
|
272
|
+
x, y = position
|
|
273
|
+
set_cursor_position(x, y)
|
|
274
|
+
else
|
|
275
|
+
set_cursor_position(position.x, position.y)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Gets the cursor position as a Position object.
|
|
280
|
+
#
|
|
281
|
+
# This is a convenience alias for +get_cursor_position+ that returns
|
|
282
|
+
# a Position object rather than raw coordinates.
|
|
283
|
+
#
|
|
284
|
+
# Returns:: A Layout::Position with the current cursor coordinates.
|
|
285
|
+
#
|
|
286
|
+
# === Example
|
|
287
|
+
#
|
|
288
|
+
#--
|
|
289
|
+
# SPDX-SnippetBegin
|
|
290
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
291
|
+
# SPDX-License-Identifier: MIT-0
|
|
292
|
+
#++
|
|
293
|
+
# pos = RatatuiRuby.cursor_position
|
|
294
|
+
# puts "Cursor at #{pos.x}, #{pos.y}"
|
|
295
|
+
#--
|
|
296
|
+
# SPDX-SnippetEnd
|
|
297
|
+
#++
|
|
298
|
+
def cursor_position
|
|
299
|
+
x, y = get_cursor_position
|
|
300
|
+
Layout::Position.new(x:, y:)
|
|
301
|
+
end
|
|
144
302
|
end
|
|
145
303
|
end
|
|
@@ -0,0 +1,197 @@
|
|
|
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
|
+
##
|
|
10
|
+
# Terminal lifecycle management for TUI sessions.
|
|
11
|
+
#
|
|
12
|
+
# This module provides methods to initialize, restore, and manage the terminal
|
|
13
|
+
# state for TUI applications. It handles raw mode, alternate screen, and ensures
|
|
14
|
+
# proper cleanup on exit.
|
|
15
|
+
#
|
|
16
|
+
# @see init_terminal
|
|
17
|
+
# @see restore_terminal
|
|
18
|
+
# @see run
|
|
19
|
+
module TerminalLifecycle
|
|
20
|
+
##
|
|
21
|
+
# Whether a TUI session is currently active.
|
|
22
|
+
#
|
|
23
|
+
# Writing to stdout/stderr during a TUI session corrupts the display.
|
|
24
|
+
# Use this to defer logging, warnings, or debug output until
|
|
25
|
+
# after the session ends.
|
|
26
|
+
#
|
|
27
|
+
# === Example
|
|
28
|
+
#
|
|
29
|
+
#--
|
|
30
|
+
# SPDX-SnippetBegin
|
|
31
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
32
|
+
# SPDX-License-Identifier: MIT-0
|
|
33
|
+
#++
|
|
34
|
+
# def log(message)
|
|
35
|
+
# if RatatuiRuby.terminal_active?
|
|
36
|
+
# @deferred_logs << message
|
|
37
|
+
# else
|
|
38
|
+
# puts message
|
|
39
|
+
# end
|
|
40
|
+
# end
|
|
41
|
+
#--
|
|
42
|
+
# SPDX-SnippetEnd
|
|
43
|
+
#++
|
|
44
|
+
def terminal_active?
|
|
45
|
+
@tui_session_active
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
##
|
|
49
|
+
# Initializes the terminal for TUI mode.
|
|
50
|
+
# Enters alternate screen and enables raw mode.
|
|
51
|
+
#
|
|
52
|
+
# In headless mode ({headless!}), this method raises {Error::Invariant}.
|
|
53
|
+
# Use headless mode for batch/CLI apps.
|
|
54
|
+
#
|
|
55
|
+
# [focus_events] whether to enable focus gain/loss events (default: true).
|
|
56
|
+
# [bracketed_paste] whether to enable bracketed paste mode (default: true).
|
|
57
|
+
#
|
|
58
|
+
# @raise [Error::Invariant] if headless mode is enabled or a session is already active
|
|
59
|
+
# @see headless!
|
|
60
|
+
def init_terminal(focus_events: true, bracketed_paste: true, viewport: nil, height: nil)
|
|
61
|
+
if @headless_mode
|
|
62
|
+
raise Error::Invariant, "Cannot initialize terminal: headless mode is enabled"
|
|
63
|
+
end
|
|
64
|
+
if @tui_session_active
|
|
65
|
+
raise Error::Invariant, "Cannot initialize terminal: TUI session already active"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Show A11Y lab prompt before launching TUI (stdout visible now, not after)
|
|
69
|
+
if Labs.enabled?(:a11y)
|
|
70
|
+
puts Labs::A11y.startup_message
|
|
71
|
+
$stdin.gets
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
@tui_session_active = true
|
|
75
|
+
|
|
76
|
+
viewport_obj = resolve_viewport(viewport, height)
|
|
77
|
+
_init_terminal(focus_events, bracketed_paste, viewport_obj.type.to_s, viewport_obj.height)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
##
|
|
81
|
+
# Initializes a test terminal for unit testing.
|
|
82
|
+
# Sets session active state like init_terminal.
|
|
83
|
+
#
|
|
84
|
+
# [width] Integer width of the test terminal.
|
|
85
|
+
# [height] Integer height of the test terminal.
|
|
86
|
+
#
|
|
87
|
+
# @raise [Error::Invariant] if headless mode is enabled or a session is already active
|
|
88
|
+
def init_test_terminal(width, height, viewport_type = "fullscreen", viewport_height = nil)
|
|
89
|
+
if @headless_mode
|
|
90
|
+
raise Error::Invariant, "Cannot initialize terminal: headless mode is enabled"
|
|
91
|
+
end
|
|
92
|
+
if @tui_session_active
|
|
93
|
+
raise Error::Invariant, "Cannot initialize terminal: TUI session already active"
|
|
94
|
+
end
|
|
95
|
+
@tui_session_active = true
|
|
96
|
+
_init_test_terminal(width, height, viewport_type, viewport_height)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
##
|
|
100
|
+
# Restores the terminal to its original state.
|
|
101
|
+
# Leaves alternate screen and disables raw mode.
|
|
102
|
+
# Also flushes any deferred warnings and panic info that were queued during the session.
|
|
103
|
+
#
|
|
104
|
+
# In headless mode ({headless!}), this method is a silent no-op since
|
|
105
|
+
# no terminal was ever initialized.
|
|
106
|
+
#
|
|
107
|
+
# @see headless!
|
|
108
|
+
def restore_terminal
|
|
109
|
+
return if @headless_mode
|
|
110
|
+
|
|
111
|
+
_restore_terminal
|
|
112
|
+
ensure
|
|
113
|
+
@tui_session_active = false
|
|
114
|
+
flush_warnings
|
|
115
|
+
flush_panic_info
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
##
|
|
119
|
+
# Starts the TUI application lifecycle.
|
|
120
|
+
#
|
|
121
|
+
# Managing generic setup/teardown (raw mode, alternate screen) manually is error-prone.
|
|
122
|
+
# If your app crashes, the terminal might be left in a broken state.
|
|
123
|
+
#
|
|
124
|
+
# This method handles the safety net. It initializes the terminal, yields a {TUI},
|
|
125
|
+
# and ensures the terminal state is restored even if exceptions occur.
|
|
126
|
+
#
|
|
127
|
+
# In headless mode ({headless!}), this method raises {Error::Invariant} immediately
|
|
128
|
+
# and the block is never executed. Use headless mode for batch/CLI apps.
|
|
129
|
+
#
|
|
130
|
+
# === Example
|
|
131
|
+
#
|
|
132
|
+
#--
|
|
133
|
+
# SPDX-SnippetBegin
|
|
134
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
135
|
+
# SPDX-License-Identifier: MIT-0
|
|
136
|
+
#++
|
|
137
|
+
# RatatuiRuby.run(focus_events: false) do |tui|
|
|
138
|
+
# tui.draw(tui.paragraph(text: "Hi"))
|
|
139
|
+
# sleep 1
|
|
140
|
+
# end
|
|
141
|
+
#
|
|
142
|
+
#--
|
|
143
|
+
# SPDX-SnippetEnd
|
|
144
|
+
#++
|
|
145
|
+
# @raise [Error::Invariant] if headless mode is enabled
|
|
146
|
+
# @see headless!
|
|
147
|
+
def run(focus_events: true, bracketed_paste: true, viewport: nil, height: nil)
|
|
148
|
+
init_terminal(focus_events:, bracketed_paste:, viewport:, height:)
|
|
149
|
+
yield TUI.new
|
|
150
|
+
ensure
|
|
151
|
+
restore_terminal
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
##
|
|
155
|
+
# Inserts content above an inline viewport.
|
|
156
|
+
#
|
|
157
|
+
# Only works with inline viewports. Content is rendered above the TUI
|
|
158
|
+
# and becomes part of terminal scrollback history.
|
|
159
|
+
#
|
|
160
|
+
# [height] Number of lines to insert (Integer)
|
|
161
|
+
# [widget] Widget to render, or nil if using block
|
|
162
|
+
# [block] Optional block that returns a widget
|
|
163
|
+
#
|
|
164
|
+
# === Example
|
|
165
|
+
#
|
|
166
|
+
#--
|
|
167
|
+
# SPDX-SnippetBegin
|
|
168
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
169
|
+
# SPDX-License-Identifier: MIT-0
|
|
170
|
+
#++
|
|
171
|
+
# RatatuiRuby.insert_before(1, Widgets::Paragraph.new(text: "Status: Done"))
|
|
172
|
+
#--
|
|
173
|
+
# SPDX-SnippetEnd
|
|
174
|
+
#++
|
|
175
|
+
def insert_before(height, widget = nil, &block)
|
|
176
|
+
# Validate we're in an inline viewport
|
|
177
|
+
viewport_type = _get_viewport_type
|
|
178
|
+
unless viewport_type == "inline"
|
|
179
|
+
raise Error::Invariant, "insert_before requires an inline viewport"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
content = widget || block&.call
|
|
183
|
+
_insert_before(height, content)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
private
|
|
187
|
+
|
|
188
|
+
def resolve_viewport(viewport, height)
|
|
189
|
+
case viewport
|
|
190
|
+
when nil, :fullscreen then Terminal::Viewport.fullscreen
|
|
191
|
+
when :inline then Terminal::Viewport.inline(height || 8)
|
|
192
|
+
when Terminal::Viewport then viewport
|
|
193
|
+
else raise ArgumentError, "Unknown viewport: #{viewport.inspect}"
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -67,7 +67,14 @@ module RatatuiRuby
|
|
|
67
67
|
# Defensive cleanup: reset any stale session state from previous test failures
|
|
68
68
|
RatatuiRuby.instance_variable_set(:@tui_session_active, false)
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
# Extract and resolve viewport
|
|
71
|
+
viewport_param = opts[:viewport]
|
|
72
|
+
viewport = case viewport_param
|
|
73
|
+
when nil then RatatuiRuby::Terminal::Viewport.fullscreen
|
|
74
|
+
when RatatuiRuby::Terminal::Viewport then viewport_param
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
RatatuiRuby.init_test_terminal(width, height, viewport.type.to_s, viewport.height)
|
|
71
78
|
# Flush any lingering events from previous tests
|
|
72
79
|
while (event = RatatuiRuby.poll_event) && !event.none?; end
|
|
73
80
|
|
|
@@ -46,6 +46,22 @@ module RatatuiRuby
|
|
|
46
46
|
def draw_cell(x, y, cell)
|
|
47
47
|
Draw.cell(x, y, cell)
|
|
48
48
|
end
|
|
49
|
+
|
|
50
|
+
# Inserts content above an inline viewport.
|
|
51
|
+
# @see RatatuiRuby.insert_before
|
|
52
|
+
def insert_before(height, widget = nil, &)
|
|
53
|
+
RatatuiRuby.insert_before(height, widget, &)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Gets the Rect of the entire terminal, regardless of viewport
|
|
57
|
+
def terminal_area
|
|
58
|
+
RatatuiRuby.terminal_area
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Gets the Rect of the viewport
|
|
62
|
+
def viewport_area
|
|
63
|
+
RatatuiRuby.viewport_area
|
|
64
|
+
end
|
|
49
65
|
end
|
|
50
66
|
end
|
|
51
67
|
end
|
data/lib/ratatui_ruby/version.rb
CHANGED
data/lib/ratatui_ruby.rb
CHANGED
|
@@ -15,6 +15,7 @@ require_relative "ratatui_ruby/buffer" # Buffer::Cell (for inspection)
|
|
|
15
15
|
require_relative "ratatui_ruby/text" # Text::Span, Text::Line, Text.width
|
|
16
16
|
require_relative "ratatui_ruby/draw" # Draw commands
|
|
17
17
|
require_relative "ratatui_ruby/symbols" # Symbols::Shade, etc.
|
|
18
|
+
require_relative "ratatui_ruby/terminal/viewport" # Terminal::Viewport
|
|
18
19
|
|
|
19
20
|
# Event types
|
|
20
21
|
require_relative "ratatui_ruby/event"
|
|
@@ -46,6 +47,9 @@ end
|
|
|
46
47
|
# Loaded after native extension so _enable_rust_backtrace is defined
|
|
47
48
|
require_relative "ratatui_ruby/debug"
|
|
48
49
|
|
|
50
|
+
# Experimental lab features (RR_LABS env var)
|
|
51
|
+
require_relative "ratatui_ruby/labs"
|
|
52
|
+
|
|
49
53
|
# Main entry point for the library.
|
|
50
54
|
#
|
|
51
55
|
# Terminal UIs require low-level control using C/Rust and high-level abstraction in Ruby.
|
|
@@ -153,6 +157,15 @@ module RatatuiRuby
|
|
|
153
157
|
# SPDX-SnippetEnd
|
|
154
158
|
#++
|
|
155
159
|
class Invariant < Error; end
|
|
160
|
+
|
|
161
|
+
# Framework bug.
|
|
162
|
+
#
|
|
163
|
+
# This error indicates a bug within the RatatuiRuby framework itself.
|
|
164
|
+
# If you encounter this, the framework is broken — please report it.
|
|
165
|
+
#
|
|
166
|
+
# Normal application errors use standard exceptions like ArgumentError.
|
|
167
|
+
# This exception class distinguishes "our bug" from "your bug".
|
|
168
|
+
class Internal < Error; end
|
|
156
169
|
end
|
|
157
170
|
|
|
158
171
|
# Mix in terminal lifecycle and output protection methods
|
|
@@ -312,7 +325,15 @@ module RatatuiRuby
|
|
|
312
325
|
if tree
|
|
313
326
|
_draw(tree)
|
|
314
327
|
elsif block
|
|
315
|
-
|
|
328
|
+
# Wrap user block to flush A11Y capture after user code
|
|
329
|
+
if Labs.enabled?(:a11y)
|
|
330
|
+
_draw do |frame|
|
|
331
|
+
block.call(frame)
|
|
332
|
+
frame.flush_a11y_capture
|
|
333
|
+
end
|
|
334
|
+
else
|
|
335
|
+
_draw(&block)
|
|
336
|
+
end
|
|
316
337
|
end
|
|
317
338
|
end
|
|
318
339
|
|
|
@@ -431,8 +452,66 @@ module RatatuiRuby
|
|
|
431
452
|
)
|
|
432
453
|
end
|
|
433
454
|
|
|
434
|
-
|
|
435
|
-
|
|
455
|
+
##
|
|
456
|
+
# Returns the current terminal viewport area.
|
|
457
|
+
#
|
|
458
|
+
# In inline viewports, this returns the viewport dimensions.
|
|
459
|
+
# In fullscreen mode, this returns the full terminal size.
|
|
460
|
+
#
|
|
461
|
+
# @return [Layout::Rect] The rendering viewport area
|
|
462
|
+
def self.get_viewport_area
|
|
463
|
+
raw = _get_terminal_area
|
|
464
|
+
Layout::Rect.new(
|
|
465
|
+
x: raw["x"],
|
|
466
|
+
y: raw["y"],
|
|
467
|
+
width: raw["width"],
|
|
468
|
+
height: raw["height"]
|
|
469
|
+
)
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
##
|
|
473
|
+
# Returns the full terminal backend size.
|
|
474
|
+
#
|
|
475
|
+
# This is always the full terminal dimensions, regardless of viewport mode.
|
|
476
|
+
#
|
|
477
|
+
# === Example
|
|
478
|
+
#
|
|
479
|
+
#--
|
|
480
|
+
# SPDX-SnippetBegin
|
|
481
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
482
|
+
# SPDX-License-Identifier: MIT-0
|
|
483
|
+
#++
|
|
484
|
+
# size = RatatuiRuby.get_terminal_size
|
|
485
|
+
# puts "Terminal: #{size.width}x#{size.height}"
|
|
486
|
+
#
|
|
487
|
+
#--
|
|
488
|
+
# SPDX-SnippetEnd
|
|
489
|
+
#++
|
|
490
|
+
# @return [Layout::Rect] The full terminal size
|
|
491
|
+
def self.get_terminal_size
|
|
492
|
+
raw = _get_terminal_size
|
|
493
|
+
Layout::Rect.new(
|
|
494
|
+
x: raw["x"],
|
|
495
|
+
y: raw["y"],
|
|
496
|
+
width: raw["width"],
|
|
497
|
+
height: raw["height"]
|
|
498
|
+
)
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
# Ruby-idiomatic aliases (TIMTOWTDI)
|
|
502
|
+
class << self
|
|
503
|
+
# Aliases for get_terminal_size (full backend size)
|
|
504
|
+
alias get_terminal_area get_terminal_size
|
|
505
|
+
alias terminal_area get_terminal_size
|
|
506
|
+
alias terminal_size get_terminal_size
|
|
507
|
+
# Aliases for get_viewport_area (viewport rendering area)
|
|
508
|
+
alias get_viewport_size get_viewport_area
|
|
509
|
+
alias viewport_area get_viewport_area
|
|
510
|
+
alias viewport_size get_viewport_area
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# (Native methods _get_cell_at and _get_terminal_size implemented in Rust)
|
|
514
|
+
private_class_method :_get_cell_at, :_get_terminal_size
|
|
436
515
|
|
|
437
516
|
# Hide native Layout._split helper
|
|
438
517
|
Layout::Layout.singleton_class.__send__(:private, :_split)
|