ratatui_ruby 0.7.4 → 0.8.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.
- 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 +17 -0
- data/doc/getting_started/quickstart.md +13 -5
- data/doc/troubleshooting/tui_output.md +85 -6
- data/examples/verify_quickstart_lifecycle/README.md +7 -1
- data/examples/verify_quickstart_lifecycle/app.rb +7 -1
- data/ext/ratatui_ruby/Cargo.lock +1 -1
- data/ext/ratatui_ruby/Cargo.toml +1 -1
- data/lib/ratatui_ruby/output_guard.rb +148 -0
- data/lib/ratatui_ruby/terminal_lifecycle.rb +129 -0
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby.rb +26 -79
- data/tasks/autodoc/examples.rb +8 -2
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1205d0ba5eb21c14b6639926e910764600c34a21f79d39a9e471c2e4902999b4
|
|
4
|
+
data.tar.gz: 3ca7b062a7320aadc1a0296b9d529adbbd449d17d1caf144d535e02b325f15c5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6311c2fabd0e5e379e41f5dc11a10e41729d5c6e3ef825706b757fb4cd35ce716e5f471a80228a5bc6120af2c6926969b5657209398d1029cf47b745fe8bc16f
|
|
7
|
+
data.tar.gz: 643c1c4b1782131bebac43bed1b3e61af125d560ea88a16018bb76754b71aa31a32fe8438f71364f48cbd55e028a2821ff545468d9b61973f4c22b7c6d53c614
|
data/.builds/ruby-3.2.yml
CHANGED
data/.builds/ruby-3.3.yml
CHANGED
data/.builds/ruby-3.4.yml
CHANGED
data/.builds/ruby-4.0.0.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,21 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
18
18
|
|
|
19
19
|
### Removed
|
|
20
20
|
|
|
21
|
+
## [0.8.0] - 2026-01-05
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- **Output Guard**: `RatatuiRuby.guard_io { }` temporarily replaces `$stdout` and `$stderr` with a null sink, preventing screen corruption from chatty gems. Active when `terminal_active?` is true; warns if called outside a session (to catch mistakes); silent no-op in headless mode.
|
|
25
|
+
- **Headless Mode**: `RatatuiRuby.headless!` enables batch/CLI mode for apps with `--no-tui` flags. When headless, `guard_io` becomes a silent no-op and `init_terminal`/`run` raise `Error::Invariant`. This allows the same code to work in both TUI and non-TUI modes.
|
|
26
|
+
- **Terminal Safety Hooks**: `at_exit` and `Signal.trap` handlers for `INT` and `TERM` automatically restore the terminal if a session is active on unexpected exit. This prevents leaving the terminal in raw mode after Ctrl+C or process termination.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- **Double `init_terminal` Now Raises `Error::Invariant` (Breaking)** Calling `init_terminal`, `init_test_terminal`, or `run` while a TUI session is already active now raises `Error::Invariant`. Previously, this was undefined but silently "working" behavior that could cause terminal state corruption. If your code intentionally double-inits, call `restore_terminal` first.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
### Removed
|
|
35
|
+
|
|
21
36
|
## [0.7.4] - 2026-01-05
|
|
22
37
|
|
|
23
38
|
### Added
|
|
@@ -26,6 +41,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
26
41
|
- **Deferred Warnings During TUI Sessions**: Experimental feature warnings (like `Paragraph#line_count`) are now automatically queued during active TUI sessions and flushed to stderr after `restore_terminal`. This prevents warnings from corrupting the TUI display. See `doc/troubleshooting/tui_output.md` for details on handling terminal output during TUI sessions.
|
|
27
42
|
- **Session State Tracking**: `RatatuiRuby.terminal_active?` indicates whether a TUI session is active. Calling `init_terminal` or `init_test_terminal` while a session is already active now raises `Error::Invariant`.
|
|
28
43
|
- **Error::Invariant**: New error class for state invariant violations (e.g., double-init). Distinct from `Error::Safety` (lifetime violations) and `Error::Terminal` (operational I/O failures).
|
|
44
|
+
|
|
29
45
|
### Changed
|
|
30
46
|
|
|
31
47
|
### Fixed
|
|
@@ -425,6 +441,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
425
441
|
- **Testing Support**: Included `RatatuiRuby::TestHelper` and RSpec integration to make testing your TUI applications possible.
|
|
426
442
|
|
|
427
443
|
[Unreleased]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/HEAD
|
|
444
|
+
[0.8.0]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/v0.8.0
|
|
428
445
|
[0.7.4]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/v0.7.4
|
|
429
446
|
[0.7.3]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/v0.7.3
|
|
430
447
|
[0.7.2]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/v0.7.2
|
|
@@ -17,7 +17,7 @@ See [Installation in the README](../README.md#installation) for setup instructio
|
|
|
17
17
|
|
|
18
18
|
Here is a "Hello World" application that demonstrates the core lifecycle of a **ratatui_ruby** app.
|
|
19
19
|
|
|
20
|
-
<!-- SYNC:START
|
|
20
|
+
<!-- SYNC:START:examples/verify_quickstart_lifecycle/app.rb:main -->
|
|
21
21
|
```ruby
|
|
22
22
|
# 1. Initialize the terminal
|
|
23
23
|
RatatuiRuby.init_terminal
|
|
@@ -51,9 +51,15 @@ begin
|
|
|
51
51
|
else
|
|
52
52
|
nil
|
|
53
53
|
end
|
|
54
|
+
|
|
55
|
+
# 5. Guard against accidental output (optional but recommended)
|
|
56
|
+
# Wrap any code that might puts/warn to prevent screen corruption.
|
|
57
|
+
RatatuiRuby.guard_io do
|
|
58
|
+
# SomeChattyGem.do_something
|
|
59
|
+
end
|
|
54
60
|
end
|
|
55
61
|
ensure
|
|
56
|
-
#
|
|
62
|
+
# 6. Restore the terminal to its original state
|
|
57
63
|
RatatuiRuby.restore_terminal
|
|
58
64
|
end
|
|
59
65
|
```
|
|
@@ -67,13 +73,14 @@ end
|
|
|
67
73
|
2. **Immediate Mode UI**: On every iteration, describe your UI by creating `Data` objects (e.g., `Paragraph`, `Block`).
|
|
68
74
|
3. **`RatatuiRuby.draw { |frame| ... }`**: The block receives a `Frame` object as a canvas. Render widgets onto specific areas. Nothing is drawn until the block finishes, ensuring flicker-free updates.
|
|
69
75
|
4. **`RatatuiRuby.poll_event`**: Returns a typed `Event` object with predicates like `key?`, `mouse?`, `resize?`, etc. Returns `RatatuiRuby::Event::None` if no events are pending. Use predicates to check event type without pattern matching.
|
|
70
|
-
5. **`RatatuiRuby.
|
|
76
|
+
5. **`RatatuiRuby.guard_io { }`**: Wraps code that might write to stdout/stderr (e.g., chatty gems). Output is swallowed to prevent screen corruption. Optional but recommended for production apps.
|
|
77
|
+
6. **`RatatuiRuby.restore_terminal`**: Essential for leaving raw mode and returning to the shell. Always wrap your loop in `begin...ensure` to guarantee this runs.
|
|
71
78
|
|
|
72
79
|
### Simplified API
|
|
73
80
|
|
|
74
81
|
You can simplify your code by using `RatatuiRuby.run`. This method handles the terminal lifecycle for you, yielding a `TUI` object with factory methods for widgets.
|
|
75
82
|
|
|
76
|
-
<!-- SYNC:START
|
|
83
|
+
<!-- SYNC:START:examples/verify_quickstart_dsl/app.rb:main -->
|
|
77
84
|
```ruby
|
|
78
85
|
# 1. Initialize the terminal, start the run loop, and ensure the terminal is restored.
|
|
79
86
|
RatatuiRuby.run do |tui|
|
|
@@ -121,7 +128,7 @@ For a deeper dive into the available application architectures (Manual vs Manage
|
|
|
121
128
|
|
|
122
129
|
Real-world applications often need to split the screen into multiple areas. `RatatuiRuby::Layout` lets you do this easily.
|
|
123
130
|
|
|
124
|
-
<!-- SYNC:START
|
|
131
|
+
<!-- SYNC:START:examples/verify_quickstart_layout/app.rb:main -->
|
|
125
132
|
```ruby
|
|
126
133
|
loop do
|
|
127
134
|
tui.draw do |frame|
|
|
@@ -255,6 +262,7 @@ Now that you've seen what **ratatui_ruby** can do:
|
|
|
255
262
|
|
|
256
263
|
- **Deep dive**: Read the [Application Architecture](../concepts/application_architecture.md) guide for scaling patterns
|
|
257
264
|
- **Test your TUI**: See the [Testing Guide](../concepts/application_testing.md) for snapshot and style assertions
|
|
265
|
+
- **Avoid common mistakes**: See [Terminal Output During TUI Sessions](../troubleshooting/tui_output.md) to prevent screen corruption
|
|
258
266
|
- **Explore the API**: Browse the [full RDoc documentation](../index.md)
|
|
259
267
|
- **Learn the philosophy**: Read [Why RatatuiRuby?](./why.md) for comparisons and design decisions
|
|
260
268
|
- **Get help**: Join the [discussion mailing list](https://lists.sr.ht/~kerrick/ratatui_ruby-discuss)
|
|
@@ -9,7 +9,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
|
|
|
9
9
|
|
|
10
10
|
Writing to stdout or stderr during a TUI session **corrupts the display**.
|
|
11
11
|
|
|
12
|
-
When your application is running inside `RatatuiRuby.run`, the terminal is in "raw mode" and RatatuiRuby has taken control of the display buffer. Any output via `puts`, `warn`, `p`, `print`, or direct writes to `
|
|
12
|
+
When your application is running inside `RatatuiRuby.run`, the terminal is in "raw mode" and RatatuiRuby has taken control of the display buffer. Any output via `puts`, `warn`, `p`, `print`, or direct writes to `$stdout`/`$stderr` will:
|
|
13
13
|
|
|
14
14
|
1. **Corrupt the screen layout** - Characters appear in random positions
|
|
15
15
|
2. **Mix with TUI output** - Text interleaves with your widgets unpredictably
|
|
@@ -24,6 +24,21 @@ In raw mode:
|
|
|
24
24
|
|
|
25
25
|
## Safe Patterns
|
|
26
26
|
|
|
27
|
+
### Use `guard_io` to swallow output from gems
|
|
28
|
+
|
|
29
|
+
If you're using a gem that might write to stdout/stderr, wrap its calls:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
RatatuiRuby.run do |tui|
|
|
33
|
+
RatatuiRuby.guard_io do
|
|
34
|
+
SomeChattyGem.do_something # Any puts/warn calls are swallowed
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Outside guard_io, you can still debug intentionally:
|
|
38
|
+
# Object::STDERR.puts "debug: something" # Escape hatch (corrupts display!)
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
27
42
|
### Defer output until after the TUI exits
|
|
28
43
|
|
|
29
44
|
```ruby
|
|
@@ -40,15 +55,21 @@ end
|
|
|
40
55
|
messages.each { |msg| puts msg }
|
|
41
56
|
```
|
|
42
57
|
|
|
43
|
-
### Use
|
|
58
|
+
### Use Logger to write to a file
|
|
59
|
+
|
|
60
|
+
The `Logger` class from Ruby's standard library is the idiomatic solution:
|
|
44
61
|
|
|
45
62
|
```ruby
|
|
46
|
-
|
|
63
|
+
require "logger"
|
|
64
|
+
require "tmpdir"
|
|
65
|
+
|
|
66
|
+
LOG = Logger.new(File.join(Dir.tmpdir, "my_app.log"))
|
|
67
|
+
LOG.level = Logger::DEBUG
|
|
47
68
|
|
|
48
69
|
RatatuiRuby.run do |tui|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
70
|
+
LOG.info "Application started"
|
|
71
|
+
LOG.debug "Processing event: #{event.inspect}"
|
|
72
|
+
|
|
52
73
|
# ... TUI logic ...
|
|
53
74
|
end
|
|
54
75
|
```
|
|
@@ -74,3 +95,61 @@ end
|
|
|
74
95
|
RatatuiRuby automatically defers its own warnings (like experimental feature notices) during TUI sessions. They are queued and printed after `restore_terminal` is called.
|
|
75
96
|
|
|
76
97
|
You don't need to do anything special for library warnings—they're handled automatically.
|
|
98
|
+
|
|
99
|
+
## Bypassing guard_io
|
|
100
|
+
|
|
101
|
+
If you need to write to stdout/stderr even when `guard_io` is active (e.g., for [pipeline integration](#headless-mode-batchpipelinecli) or IPC), use the original IO constants:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
RatatuiRuby.guard_io do
|
|
105
|
+
SomeChattyGem.do_something # This is swallowed
|
|
106
|
+
|
|
107
|
+
# But this gets through:
|
|
108
|
+
Object::STDOUT.puts "structured output for downstream tools"
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
This works regardless of whether `guard_io` is active. During a TUI session, the display will be corrupted—but the output will reach its destination.
|
|
113
|
+
|
|
114
|
+
## Headless Mode (Batch/Pipeline/CLI)
|
|
115
|
+
|
|
116
|
+
If your app supports both TUI and non-TUI modes (e.g., `my_app --no-tui`), call `headless!` at startup to silence `guard_io` warnings:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
if ARGV.include?("--no-tui")
|
|
120
|
+
RatatuiRuby.headless!
|
|
121
|
+
# guard_io calls are now silent no-ops
|
|
122
|
+
process_batch_work
|
|
123
|
+
else
|
|
124
|
+
RatatuiRuby.run do |tui|
|
|
125
|
+
# TUI mode - guard_io works normally
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
When headless, `guard_io` becomes a no-op (output flows normally), and calling `run` or `init_terminal` raises an error.
|
|
131
|
+
|
|
132
|
+
## Temporarily Exiting TUI Mode
|
|
133
|
+
|
|
134
|
+
Some apps need to temporarily leave TUI mode for user interaction—like lazygit does when opening an external editor for commit messages. Use `restore_terminal` and `init_terminal`:
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
RatatuiRuby.run do |tui|
|
|
138
|
+
# ... TUI is active ...
|
|
139
|
+
|
|
140
|
+
if user_wants_external_editor
|
|
141
|
+
RatatuiRuby.restore_terminal
|
|
142
|
+
|
|
143
|
+
# Now in normal terminal mode
|
|
144
|
+
system("$EDITOR", filename)
|
|
145
|
+
puts "Press Enter to return to the TUI..."
|
|
146
|
+
gets
|
|
147
|
+
|
|
148
|
+
RatatuiRuby.init_terminal
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# ... TUI is active again ...
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
This pattern lets you hand control back to the user or spawn external processes that need normal terminal access.
|
|
@@ -45,9 +45,15 @@ begin
|
|
|
45
45
|
else
|
|
46
46
|
nil
|
|
47
47
|
end
|
|
48
|
+
|
|
49
|
+
# 5. Guard against accidental output (optional but recommended)
|
|
50
|
+
# Wrap any code that might puts/warn to prevent screen corruption.
|
|
51
|
+
RatatuiRuby.guard_io do
|
|
52
|
+
# SomeChattyGem.do_something
|
|
53
|
+
end
|
|
48
54
|
end
|
|
49
55
|
ensure
|
|
50
|
-
#
|
|
56
|
+
# 6. Restore the terminal to its original state
|
|
51
57
|
RatatuiRuby.restore_terminal
|
|
52
58
|
end
|
|
53
59
|
```
|
|
@@ -44,9 +44,15 @@ class VerifyQuickstartLifecycle
|
|
|
44
44
|
else
|
|
45
45
|
nil
|
|
46
46
|
end
|
|
47
|
+
|
|
48
|
+
# 5. Guard against accidental output (optional but recommended)
|
|
49
|
+
# Wrap any code that might puts/warn to prevent screen corruption.
|
|
50
|
+
RatatuiRuby.guard_io do
|
|
51
|
+
# SomeChattyGem.do_something
|
|
52
|
+
end
|
|
47
53
|
end
|
|
48
54
|
ensure
|
|
49
|
-
#
|
|
55
|
+
# 6. Restore the terminal to its original state
|
|
50
56
|
RatatuiRuby.restore_terminal
|
|
51
57
|
end
|
|
52
58
|
# [SYNC:END:main]
|
data/ext/ratatui_ruby/Cargo.lock
CHANGED
data/ext/ratatui_ruby/Cargo.toml
CHANGED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
#++
|
|
8
|
+
|
|
9
|
+
module RatatuiRuby
|
|
10
|
+
##
|
|
11
|
+
# Output protection for TUI sessions and batch/CLI mode.
|
|
12
|
+
#
|
|
13
|
+
# This module provides mechanisms to prevent accidental output to stdout/stderr
|
|
14
|
+
# during TUI sessions and to support headless (batch/CLI) mode for applications
|
|
15
|
+
# that can run with or without a TUI.
|
|
16
|
+
#
|
|
17
|
+
# @see guard_io
|
|
18
|
+
# @see headless!
|
|
19
|
+
module OutputGuard
|
|
20
|
+
# A null IO object that swallows all output.
|
|
21
|
+
#
|
|
22
|
+
# Used by {guard_io} to temporarily replace $stdout and $stderr.
|
|
23
|
+
# Implements method_missing to accept any IO method and discard output.
|
|
24
|
+
#
|
|
25
|
+
# Returns self for method chaining (e.g., puts.flush).
|
|
26
|
+
class NullIO
|
|
27
|
+
# Accepts any method call and returns self, discarding all output.
|
|
28
|
+
def method_missing(name, *args, &block)
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Reports that all methods are supported.
|
|
33
|
+
def respond_to_missing?(name, include_private = false)
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
##
|
|
39
|
+
# Whether headless (batch/pipeline/CLI) mode is enabled.
|
|
40
|
+
#
|
|
41
|
+
# When headless mode is active:
|
|
42
|
+
# - {guard_io} becomes a silent no-op (output is not swallowed)
|
|
43
|
+
# - {init_terminal} and {run} raise {Error::Invariant}
|
|
44
|
+
#
|
|
45
|
+
# Use this when your app has a `--no-tui` or `--batch` flag and you want
|
|
46
|
+
# the same code to work in both TUI and non-TUI modes.
|
|
47
|
+
#
|
|
48
|
+
# @see headless!
|
|
49
|
+
def is_headless?
|
|
50
|
+
@headless_mode
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
##
|
|
54
|
+
# Enables headless (batch/CLI) mode.
|
|
55
|
+
#
|
|
56
|
+
# Call this at app startup when running in batch/CLI mode (e.g., `--no-tui`).
|
|
57
|
+
# This tells RatatuiRuby that you intentionally don't want a TUI session.
|
|
58
|
+
#
|
|
59
|
+
# When headless mode is active:
|
|
60
|
+
# - {guard_io} becomes a silent no-op (output flows normally)
|
|
61
|
+
# - {init_terminal} and {run} raise {Error::Invariant}
|
|
62
|
+
#
|
|
63
|
+
# Headless mode and TUI sessions are mutually exclusive. Calling this
|
|
64
|
+
# while a TUI session is active raises {Error::Invariant}.
|
|
65
|
+
#
|
|
66
|
+
# === Why there is no exit_headless!
|
|
67
|
+
#
|
|
68
|
+
# Headless mode is a startup-time decision for the entire app run.
|
|
69
|
+
# If you need to temporarily exit TUI mode for user interaction
|
|
70
|
+
# (like lazygit does when editing a commit message), use
|
|
71
|
+
# {restore_terminal} and {init_terminal} instead:
|
|
72
|
+
#
|
|
73
|
+
# RatatuiRuby.restore_terminal
|
|
74
|
+
# puts "Press enter to continue..."
|
|
75
|
+
# gets
|
|
76
|
+
# RatatuiRuby.init_terminal
|
|
77
|
+
#
|
|
78
|
+
# === Example
|
|
79
|
+
#
|
|
80
|
+
# if ARGV.include?("--no-tui")
|
|
81
|
+
# RatatuiRuby.headless!
|
|
82
|
+
# process_batch_work # guard_io calls are silent no-ops
|
|
83
|
+
# else
|
|
84
|
+
# RatatuiRuby.run do |tui| # This branch only runs in TUI mode
|
|
85
|
+
# # ... TUI code ...
|
|
86
|
+
# end
|
|
87
|
+
# end
|
|
88
|
+
#
|
|
89
|
+
# Note: Calling {run} or {init_terminal} after {headless!} raises
|
|
90
|
+
# {Error::Invariant}. The block is never executed.
|
|
91
|
+
#
|
|
92
|
+
# @raise [Error::Invariant] if a TUI session is already active
|
|
93
|
+
# @see is_headless?
|
|
94
|
+
# @see restore_terminal
|
|
95
|
+
def headless!
|
|
96
|
+
if @tui_session_active
|
|
97
|
+
raise Error::Invariant, "Cannot enable headless mode: TUI session already active"
|
|
98
|
+
end
|
|
99
|
+
@headless_mode = true
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
##
|
|
103
|
+
# Guards a block from stdout/stderr output.
|
|
104
|
+
#
|
|
105
|
+
# During a TUI session, writes to $stdout or $stderr corrupt the display.
|
|
106
|
+
# Wrap code that might produce output (e.g., chatty gems) in this block.
|
|
107
|
+
#
|
|
108
|
+
# This temporarily replaces $stdout and $stderr with a {NullIO} object
|
|
109
|
+
# that discards all output. The original streams are restored when the
|
|
110
|
+
# block exits, even if an exception occurs.
|
|
111
|
+
#
|
|
112
|
+
# === Behavior by mode
|
|
113
|
+
#
|
|
114
|
+
# - **TUI session active**: Output is swallowed (guarded)
|
|
115
|
+
# - **Headless mode**: Silent no-op (output flows normally)
|
|
116
|
+
# - **Neither**: Warns and yields (catches potential mistakes)
|
|
117
|
+
#
|
|
118
|
+
# === Example
|
|
119
|
+
#
|
|
120
|
+
# RatatuiRuby.run do |tui|
|
|
121
|
+
# RatatuiRuby.guard_io do
|
|
122
|
+
# SomeChattyGem.do_something # Any puts/warn calls are swallowed
|
|
123
|
+
# end
|
|
124
|
+
# end
|
|
125
|
+
#
|
|
126
|
+
# @see headless!
|
|
127
|
+
def guard_io
|
|
128
|
+
# TUI active: guard the output
|
|
129
|
+
if terminal_active?
|
|
130
|
+
$stdout = NullIO.new
|
|
131
|
+
$stderr = NullIO.new
|
|
132
|
+
begin
|
|
133
|
+
return yield
|
|
134
|
+
ensure
|
|
135
|
+
$stdout = Object::STDOUT
|
|
136
|
+
$stderr = Object::STDERR
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Headless mode: silent no-op
|
|
141
|
+
return yield if is_headless?
|
|
142
|
+
|
|
143
|
+
# Neither: warn about potential mistake
|
|
144
|
+
warn "guard_io called outside TUI session. If this is intentional (batch/CLI mode), call RatatuiRuby.headless! at startup to silence this warning."
|
|
145
|
+
yield
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
#++
|
|
8
|
+
|
|
9
|
+
module RatatuiRuby
|
|
10
|
+
##
|
|
11
|
+
# Terminal lifecycle management for TUI sessions.
|
|
12
|
+
#
|
|
13
|
+
# This module provides methods to initialize, restore, and manage the terminal
|
|
14
|
+
# state for TUI applications. It handles raw mode, alternate screen, and ensures
|
|
15
|
+
# proper cleanup on exit.
|
|
16
|
+
#
|
|
17
|
+
# @see init_terminal
|
|
18
|
+
# @see restore_terminal
|
|
19
|
+
# @see run
|
|
20
|
+
module TerminalLifecycle
|
|
21
|
+
##
|
|
22
|
+
# Whether a TUI session is currently active.
|
|
23
|
+
#
|
|
24
|
+
# Writing to stdout/stderr during a TUI session corrupts the display.
|
|
25
|
+
# Use this to defer logging, warnings, or debug output until
|
|
26
|
+
# after the session ends.
|
|
27
|
+
#
|
|
28
|
+
# === Example
|
|
29
|
+
#
|
|
30
|
+
# def log(message)
|
|
31
|
+
# if RatatuiRuby.terminal_active?
|
|
32
|
+
# @deferred_logs << message
|
|
33
|
+
# else
|
|
34
|
+
# puts message
|
|
35
|
+
# end
|
|
36
|
+
# end
|
|
37
|
+
def terminal_active?
|
|
38
|
+
@tui_session_active
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
##
|
|
42
|
+
# Initializes the terminal for TUI mode.
|
|
43
|
+
# Enters alternate screen and enables raw mode.
|
|
44
|
+
#
|
|
45
|
+
# In headless mode ({headless!}), this method raises {Error::Invariant}.
|
|
46
|
+
# Use headless mode for batch/CLI apps.
|
|
47
|
+
#
|
|
48
|
+
# [focus_events] whether to enable focus gain/loss events (default: true).
|
|
49
|
+
# [bracketed_paste] whether to enable bracketed paste mode (default: true).
|
|
50
|
+
#
|
|
51
|
+
# @raise [Error::Invariant] if headless mode is enabled or a session is already active
|
|
52
|
+
# @see headless!
|
|
53
|
+
def init_terminal(focus_events: true, bracketed_paste: true)
|
|
54
|
+
if @headless_mode
|
|
55
|
+
raise Error::Invariant, "Cannot initialize terminal: headless mode is enabled"
|
|
56
|
+
end
|
|
57
|
+
if @tui_session_active
|
|
58
|
+
raise Error::Invariant, "Cannot initialize terminal: TUI session already active"
|
|
59
|
+
end
|
|
60
|
+
@tui_session_active = true
|
|
61
|
+
_init_terminal(focus_events, bracketed_paste)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
##
|
|
65
|
+
# Initializes a test terminal for unit testing.
|
|
66
|
+
# Sets session active state like init_terminal.
|
|
67
|
+
#
|
|
68
|
+
# [width] Integer width of the test terminal.
|
|
69
|
+
# [height] Integer height of the test terminal.
|
|
70
|
+
#
|
|
71
|
+
# @raise [Error::Invariant] if headless mode is enabled or a session is already active
|
|
72
|
+
def init_test_terminal(width, height)
|
|
73
|
+
if @headless_mode
|
|
74
|
+
raise Error::Invariant, "Cannot initialize terminal: headless mode is enabled"
|
|
75
|
+
end
|
|
76
|
+
if @tui_session_active
|
|
77
|
+
raise Error::Invariant, "Cannot initialize terminal: TUI session already active"
|
|
78
|
+
end
|
|
79
|
+
@tui_session_active = true
|
|
80
|
+
_init_test_terminal(width, height)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
##
|
|
84
|
+
# Restores the terminal to its original state.
|
|
85
|
+
# Leaves alternate screen and disables raw mode.
|
|
86
|
+
# Also flushes any deferred warnings that were queued during the session.
|
|
87
|
+
#
|
|
88
|
+
# In headless mode ({headless!}), this method is a silent no-op since
|
|
89
|
+
# no terminal was ever initialized.
|
|
90
|
+
#
|
|
91
|
+
# @see headless!
|
|
92
|
+
def restore_terminal
|
|
93
|
+
return if @headless_mode
|
|
94
|
+
|
|
95
|
+
_restore_terminal
|
|
96
|
+
ensure
|
|
97
|
+
@tui_session_active = false
|
|
98
|
+
flush_warnings
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
##
|
|
102
|
+
# Starts the TUI application lifecycle.
|
|
103
|
+
#
|
|
104
|
+
# Managing generic setup/teardown (raw mode, alternate screen) manually is error-prone.
|
|
105
|
+
# If your app crashes, the terminal might be left in a broken state.
|
|
106
|
+
#
|
|
107
|
+
# This method handles the safety net. It initializes the terminal, yields a {TUI},
|
|
108
|
+
# and ensures the terminal state is restored even if exceptions occur.
|
|
109
|
+
#
|
|
110
|
+
# In headless mode ({headless!}), this method raises {Error::Invariant} immediately
|
|
111
|
+
# and the block is never executed. Use headless mode for batch/CLI apps.
|
|
112
|
+
#
|
|
113
|
+
# === Example
|
|
114
|
+
#
|
|
115
|
+
# RatatuiRuby.run(focus_events: false) do |tui|
|
|
116
|
+
# tui.draw(tui.paragraph(text: "Hi"))
|
|
117
|
+
# sleep 1
|
|
118
|
+
# end
|
|
119
|
+
#
|
|
120
|
+
# @raise [Error::Invariant] if headless mode is enabled
|
|
121
|
+
# @see headless!
|
|
122
|
+
def run(focus_events: true, bracketed_paste: true)
|
|
123
|
+
init_terminal(focus_events:, bracketed_paste:)
|
|
124
|
+
yield TUI.new
|
|
125
|
+
ensure
|
|
126
|
+
restore_terminal
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
data/lib/ratatui_ruby/version.rb
CHANGED
data/lib/ratatui_ruby.rb
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
require_relative "ratatui_ruby/version"
|
|
9
9
|
|
|
10
|
-
#
|
|
10
|
+
# Core types (mirrors ratatui Rust crate)
|
|
11
11
|
require_relative "ratatui_ruby/layout" # Layout::Rect, Layout::Constraint, Layout::Layout
|
|
12
12
|
require_relative "ratatui_ruby/style" # Style::Style
|
|
13
13
|
require_relative "ratatui_ruby/widgets" # Widgets::Block, Widgets::Paragraph, etc.
|
|
@@ -24,6 +24,10 @@ require_relative "ratatui_ruby/list_state"
|
|
|
24
24
|
require_relative "ratatui_ruby/table_state"
|
|
25
25
|
require_relative "ratatui_ruby/scrollbar_state"
|
|
26
26
|
|
|
27
|
+
# Behavioral mixins
|
|
28
|
+
require_relative "ratatui_ruby/output_guard"
|
|
29
|
+
require_relative "ratatui_ruby/terminal_lifecycle"
|
|
30
|
+
|
|
27
31
|
# TUI facade (for external instantiation and caching)
|
|
28
32
|
require_relative "ratatui_ruby/tui"
|
|
29
33
|
|
|
@@ -110,44 +114,18 @@ module RatatuiRuby
|
|
|
110
114
|
class Invariant < Error; end
|
|
111
115
|
end
|
|
112
116
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
#
|
|
118
|
-
|
|
119
|
-
def self.init_terminal(focus_events: true, bracketed_paste: true)
|
|
120
|
-
if @tui_session_active
|
|
121
|
-
raise Error::Invariant, "Cannot initialize terminal: TUI session already active"
|
|
122
|
-
end
|
|
123
|
-
@tui_session_active = true
|
|
124
|
-
_init_terminal(focus_events, bracketed_paste)
|
|
125
|
-
end
|
|
117
|
+
# Mix in terminal lifecycle and output protection methods
|
|
118
|
+
extend OutputGuard
|
|
119
|
+
extend TerminalLifecycle
|
|
120
|
+
|
|
121
|
+
# Re-export NullIO at module root for backward compatibility
|
|
122
|
+
NullIO = OutputGuard::NullIO
|
|
126
123
|
|
|
127
124
|
@experimental_warnings = true
|
|
128
125
|
@tui_session_active = false
|
|
126
|
+
@headless_mode = false
|
|
129
127
|
@deferred_warnings = []
|
|
130
128
|
|
|
131
|
-
##
|
|
132
|
-
# Whether a TUI session is currently active.
|
|
133
|
-
#
|
|
134
|
-
# Writing to stdout/stderr during a TUI session corrupts the display.
|
|
135
|
-
# Use this to defer logging, warnings, or debug output until
|
|
136
|
-
# after the session ends.
|
|
137
|
-
#
|
|
138
|
-
# === Example
|
|
139
|
-
#
|
|
140
|
-
# def log(message)
|
|
141
|
-
# if RatatuiRuby.terminal_active?
|
|
142
|
-
# @deferred_logs << message
|
|
143
|
-
# else
|
|
144
|
-
# puts message
|
|
145
|
-
# end
|
|
146
|
-
# end
|
|
147
|
-
def self.terminal_active?
|
|
148
|
-
@tui_session_active
|
|
149
|
-
end
|
|
150
|
-
|
|
151
129
|
class << self
|
|
152
130
|
##
|
|
153
131
|
# :attr_accessor: experimental_warnings
|
|
@@ -165,17 +143,6 @@ module RatatuiRuby
|
|
|
165
143
|
end
|
|
166
144
|
end
|
|
167
145
|
|
|
168
|
-
##
|
|
169
|
-
# Restores the terminal to its original state.
|
|
170
|
-
# Leaves alternate screen and disables raw mode.
|
|
171
|
-
# Also flushes any deferred warnings that were queued during the session.
|
|
172
|
-
def self.restore_terminal
|
|
173
|
-
_restore_terminal
|
|
174
|
-
ensure
|
|
175
|
-
@tui_session_active = false
|
|
176
|
-
flush_warnings
|
|
177
|
-
end
|
|
178
|
-
|
|
179
146
|
##
|
|
180
147
|
# :singleton-method: inject_test_event
|
|
181
148
|
# Injects a mock event into the event queue for testing purposes.
|
|
@@ -207,20 +174,6 @@ module RatatuiRuby
|
|
|
207
174
|
@warned_features[feature_name] = true
|
|
208
175
|
end
|
|
209
176
|
|
|
210
|
-
##
|
|
211
|
-
# Initializes a test terminal for unit testing.
|
|
212
|
-
# Sets session active state like init_terminal.
|
|
213
|
-
#
|
|
214
|
-
# [width] Integer width of the test terminal.
|
|
215
|
-
# [height] Integer height of the test terminal.
|
|
216
|
-
def self.init_test_terminal(width, height)
|
|
217
|
-
if @tui_session_active
|
|
218
|
-
raise Error::Invariant, "Cannot initialize terminal: TUI session already active"
|
|
219
|
-
end
|
|
220
|
-
@tui_session_active = true
|
|
221
|
-
_init_test_terminal(width, height)
|
|
222
|
-
end
|
|
223
|
-
|
|
224
177
|
# (Native methods implemented in Rust)
|
|
225
178
|
private_class_method :_init_terminal, :_restore_terminal, :_init_test_terminal
|
|
226
179
|
|
|
@@ -328,26 +281,6 @@ module RatatuiRuby
|
|
|
328
281
|
# (Native method _poll_event implemented in Rust)
|
|
329
282
|
private_class_method :_poll_event
|
|
330
283
|
|
|
331
|
-
##
|
|
332
|
-
# Starts the TUI application lifecycle.
|
|
333
|
-
#
|
|
334
|
-
# Managing generic setup/teardown (raw mode, alternate screen) manualy is error-prone. If your app crashes, the terminal might be left in a broken state.
|
|
335
|
-
#
|
|
336
|
-
# This method handles the safety net. It initializes the terminal, yields a {TUI}, and ensures the terminal state is restored even if exceptions occur.
|
|
337
|
-
#
|
|
338
|
-
# === Example
|
|
339
|
-
#
|
|
340
|
-
# RatatuiRuby.run(focus_events: false) do |tui|
|
|
341
|
-
# tui.draw(tui.paragraph(text: "Hi"))
|
|
342
|
-
# sleep 1
|
|
343
|
-
# end
|
|
344
|
-
def self.run(focus_events: true, bracketed_paste: true)
|
|
345
|
-
init_terminal(focus_events:, bracketed_paste:)
|
|
346
|
-
yield TUI.new
|
|
347
|
-
ensure
|
|
348
|
-
restore_terminal
|
|
349
|
-
end
|
|
350
|
-
|
|
351
284
|
##
|
|
352
285
|
# Inspects the terminal buffer at specific coordinates.
|
|
353
286
|
#
|
|
@@ -380,4 +313,18 @@ module RatatuiRuby
|
|
|
380
313
|
|
|
381
314
|
# Hide native Layout._split helper
|
|
382
315
|
Layout::Layout.singleton_class.__send__(:private, :_split)
|
|
316
|
+
|
|
317
|
+
# --- Terminal Safety Hooks ---
|
|
318
|
+
# These ensure the terminal is restored even on unexpected exits.
|
|
319
|
+
|
|
320
|
+
at_exit do
|
|
321
|
+
restore_terminal if terminal_active?
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
%i[INT TERM].each do |signal|
|
|
325
|
+
trap(signal) do
|
|
326
|
+
restore_terminal if terminal_active?
|
|
327
|
+
exit(128 + Signal.list[signal.to_s])
|
|
328
|
+
end
|
|
329
|
+
end
|
|
383
330
|
end
|
data/tasks/autodoc/examples.rb
CHANGED
|
@@ -12,7 +12,7 @@ module Autodoc
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def sync
|
|
15
|
-
Dir.glob("{README.md,doc
|
|
15
|
+
Dir.glob("{README.md,doc/**/*.md,examples/*/README.md}").each do |readme_path|
|
|
16
16
|
sync_readme(readme_path)
|
|
17
17
|
end
|
|
18
18
|
end
|
|
@@ -24,7 +24,13 @@ module Autodoc
|
|
|
24
24
|
new_content = content.gsub(/<!-- SYNC:START:([^ ]+) -->.*?<!-- SYNC:END -->/m) do
|
|
25
25
|
marker_info = $1
|
|
26
26
|
source_rel_path, segment_id = marker_info.split(":")
|
|
27
|
-
|
|
27
|
+
|
|
28
|
+
# Support both repo-root-relative paths (no leading ./) and file-relative paths
|
|
29
|
+
source_path = if source_rel_path.start_with?("./", "../")
|
|
30
|
+
File.join(dir, source_rel_path)
|
|
31
|
+
else
|
|
32
|
+
source_rel_path # Already relative to repo root
|
|
33
|
+
end
|
|
28
34
|
|
|
29
35
|
unless File.exist?(source_path)
|
|
30
36
|
warn "Warning: Source file not found: #{source_path}"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ratatui_ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kerrick Long
|
|
@@ -317,6 +317,7 @@ files:
|
|
|
317
317
|
- lib/ratatui_ruby/layout/layout.rb
|
|
318
318
|
- lib/ratatui_ruby/layout/rect.rb
|
|
319
319
|
- lib/ratatui_ruby/list_state.rb
|
|
320
|
+
- lib/ratatui_ruby/output_guard.rb
|
|
320
321
|
- lib/ratatui_ruby/schema/bar_chart.rb
|
|
321
322
|
- lib/ratatui_ruby/schema/bar_chart/bar.rb
|
|
322
323
|
- lib/ratatui_ruby/schema/bar_chart/bar_group.rb
|
|
@@ -351,6 +352,7 @@ files:
|
|
|
351
352
|
- lib/ratatui_ruby/style.rb
|
|
352
353
|
- lib/ratatui_ruby/style/style.rb
|
|
353
354
|
- lib/ratatui_ruby/table_state.rb
|
|
355
|
+
- lib/ratatui_ruby/terminal_lifecycle.rb
|
|
354
356
|
- lib/ratatui_ruby/test_helper.rb
|
|
355
357
|
- lib/ratatui_ruby/test_helper/event_injection.rb
|
|
356
358
|
- lib/ratatui_ruby/test_helper/snapshot.rb
|