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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8c89b519ce8bf221f4f21ad0b1032e2d7e11214e9df530fc37cdd456f908889
4
- data.tar.gz: f188fbed117ef8758f2dcdca0c56b837e424de84fea2a2a9ee4027685ccad41e
3
+ metadata.gz: 1205d0ba5eb21c14b6639926e910764600c34a21f79d39a9e471c2e4902999b4
4
+ data.tar.gz: 3ca7b062a7320aadc1a0296b9d529adbbd449d17d1caf144d535e02b325f15c5
5
5
  SHA512:
6
- metadata.gz: 9955835cd59fd2c8e13f3d0f8f9b284b140f1887d529fb1e1bbb4b39941a37259fff0159791a0fac87c105797ba0dc31b17ddfe0582a71a4efe7461e1c6a0a12
7
- data.tar.gz: ff2b0011104b3edf8dd20ee7dcce5c622772f2d9820e70d66f27738aee20a71f3eff29129a1c9feb4e6c81beeab75d724cc7b7fbe3214f28430458861a15206a
6
+ metadata.gz: 6311c2fabd0e5e379e41f5dc11a10e41729d5c6e3ef825706b757fb4cd35ce716e5f471a80228a5bc6120af2c6926969b5657209398d1029cf47b745fe8bc16f
7
+ data.tar.gz: 643c1c4b1782131bebac43bed1b3e61af125d560ea88a16018bb76754b71aa31a32fe8438f71364f48cbd55e028a2821ff545468d9b61973f4c22b7c6d53c614
data/.builds/ruby-3.2.yml CHANGED
@@ -16,7 +16,7 @@ packages:
16
16
  - clang
17
17
  - git
18
18
  artifacts:
19
- - ratatui_ruby/pkg/ratatui_ruby-0.7.4.gem
19
+ - ratatui_ruby/pkg/ratatui_ruby-0.8.0.gem
20
20
  sources:
21
21
  - https://git.sr.ht/~kerrick/ratatui_ruby
22
22
  tasks:
data/.builds/ruby-3.3.yml CHANGED
@@ -16,7 +16,7 @@ packages:
16
16
  - clang
17
17
  - git
18
18
  artifacts:
19
- - ratatui_ruby/pkg/ratatui_ruby-0.7.4.gem
19
+ - ratatui_ruby/pkg/ratatui_ruby-0.8.0.gem
20
20
  sources:
21
21
  - https://git.sr.ht/~kerrick/ratatui_ruby
22
22
  tasks:
data/.builds/ruby-3.4.yml CHANGED
@@ -16,7 +16,7 @@ packages:
16
16
  - clang
17
17
  - git
18
18
  artifacts:
19
- - ratatui_ruby/pkg/ratatui_ruby-0.7.4.gem
19
+ - ratatui_ruby/pkg/ratatui_ruby-0.8.0.gem
20
20
  sources:
21
21
  - https://git.sr.ht/~kerrick/ratatui_ruby
22
22
  tasks:
@@ -16,7 +16,7 @@ packages:
16
16
  - clang
17
17
  - git
18
18
  artifacts:
19
- - ratatui_ruby/pkg/ratatui_ruby-0.7.4.gem
19
+ - ratatui_ruby/pkg/ratatui_ruby-0.8.0.gem
20
20
  sources:
21
21
  - https://git.sr.ht/~kerrick/ratatui_ruby
22
22
  tasks:
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:../examples/verify_quickstart_lifecycle/app.rb:main -->
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
- # 5. Restore the terminal to its original state
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.restore_terminal`**: Essential for leaving raw mode and returning to the shell. Always wrap your loop in `begin...ensure` to guarantee this runs.
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:../examples/verify_quickstart_dsl/app.rb:main -->
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:../examples/verify_quickstart_layout/app.rb:main -->
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 `STDOUT`/`STDERR` will:
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 debug logging to a file
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
- DEBUG_LOG = File.open("/tmp/my_app_debug.log", "a")
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
- DEBUG_LOG.puts "Debug: something happened"
50
- DEBUG_LOG.flush
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
- # 5. Restore the terminal to its original state
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
- # 5. Restore the terminal to its original state
55
+ # 6. Restore the terminal to its original state
50
56
  RatatuiRuby.restore_terminal
51
57
  end
52
58
  # [SYNC:END:main]
@@ -1012,7 +1012,7 @@ dependencies = [
1012
1012
 
1013
1013
  [[package]]
1014
1014
  name = "ratatui_ruby"
1015
- version = "0.7.4"
1015
+ version = "0.8.0"
1016
1016
  dependencies = [
1017
1017
  "bumpalo",
1018
1018
  "lazy_static",
@@ -3,7 +3,7 @@
3
3
 
4
4
  [package]
5
5
  name = "ratatui_ruby"
6
- version = "0.7.4"
6
+ version = "0.8.0"
7
7
  edition = "2021"
8
8
 
9
9
  [lib]
@@ -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
@@ -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 = "0.7.4"
11
+ VERSION = "0.8.0"
12
12
  end
data/lib/ratatui_ruby.rb CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  require_relative "ratatui_ruby/version"
9
9
 
10
- # New modularized structure (mirrors ratatui Rust crate)
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
- # Initializes the terminal for TUI mode.
115
- # Enters alternate screen and enables raw mode.
116
- #
117
- # [focus_events] whether to enable focus gain/loss events (default: true).
118
- # [bracketed_paste] whether to enable bracketed paste mode (default: true).
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
@@ -12,7 +12,7 @@ module Autodoc
12
12
  end
13
13
 
14
14
  def sync
15
- Dir.glob("{README.md,doc/*.md,examples/*/README.md}").each do |readme_path|
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
- source_path = File.join(dir, source_rel_path)
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.7.4
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