Skip to content

Commit 0137ea2

Browse files
authored
fix(core): avoid panic when inline tui can't init (#34135)
## Current Behavior if inline tui init fails, we panic ## Expected Behavior If inline tui init fails, inline mode is disabled. We show the reason its disabled when someone tries to use it. ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #
1 parent 6754cde commit 0137ea2

3 files changed

Lines changed: 48 additions & 35 deletions

File tree

packages/nx/src/native/tui/app.rs

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ impl App {
542542
if matches!(key.code, KeyCode::F(11))
543543
&& !matches!(self.focus, Focus::CountdownPopup)
544544
{
545-
self.try_switch_to_inline_mode();
545+
self.dispatch_action(Action::SwitchMode(TuiMode::Inline));
546546
return Ok(false);
547547
}
548548

@@ -744,9 +744,7 @@ impl App {
744744
// swap to inline tui mode focusing that task
745745
KeyCode::Enter => {
746746
// dispatch action to switch to inline mode with focused task
747-
if let Focus::MultipleOutput(_pane_idx) = self.focus {
748-
self.try_switch_to_inline_mode();
749-
}
747+
self.dispatch_action(Action::SwitchMode(TuiMode::Inline));
750748
}
751749
_ => {
752750
// Forward other keys for interactivity, scrolling (j/k) etc
@@ -1266,22 +1264,6 @@ impl App {
12661264
self.core.dispatch_action(action);
12671265
}
12681266

1269-
/// Attempts to switch to inline mode, showing a hint if unavailable
1270-
///
1271-
/// Inline mode requires stdin to be a TTY for cursor position queries.
1272-
/// In environments like git hooks where stdin is redirected, inline mode
1273-
/// would hang waiting for terminal responses. This method shows a helpful
1274-
/// hint instead of silently failing.
1275-
fn try_switch_to_inline_mode(&self) {
1276-
if tui::Tui::is_stdin_interactive() {
1277-
self.dispatch_action(Action::SwitchMode(TuiMode::Inline));
1278-
} else {
1279-
self.dispatch_action(Action::ShowHint(
1280-
"Inline mode is not available in this environment (stdin is not a TTY)".to_string(),
1281-
));
1282-
}
1283-
}
1284-
12851267
fn recalculate_layout_areas(&mut self) {
12861268
if let Some(frame_area) = self.frame_area {
12871269
self.layout_areas = Some(self.layout_manager.calculate_layout(frame_area));

packages/nx/src/native/tui/lifecycle.rs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -180,16 +180,21 @@ fn switch_mode(
180180
// This happens in git hooks where stderr is a TTY but stdin is redirected.
181181
// In this case, show a hint to the user and stay in fullscreen mode instead of hanging.
182182
// See: https://github.com/crossterm-rs/crossterm/issues/692
183-
if target_mode == TuiMode::Inline && !super::tui::Tui::is_stdin_interactive() {
184-
debug!(
185-
"Cannot switch to inline mode: stdin is not a TTY. \
186-
Staying in fullscreen mode to avoid hanging on cursor position query."
187-
);
188-
// Show hint to inform user why inline mode is unavailable
189-
let _ = action_tx.send(Action::ShowHint(
190-
"Inline mode is not available in this environment (stdin is not a TTY)".to_string(),
191-
));
192-
return Some(tui.current_mode);
183+
if target_mode == TuiMode::Inline {
184+
let inline_tui_unsupported = tui.inline_tui_unsupported_reason();
185+
if let Some(reason) = inline_tui_unsupported {
186+
debug!(
187+
"Cannot switch to inline mode: {}. \
188+
Staying in fullscreen mode to avoid hanging on cursor position query.",
189+
reason
190+
);
191+
// Show hint to inform user why inline mode is unavailable
192+
let _ = action_tx.send(Action::ShowHint(format!(
193+
"Inline mode is not available in this environment: {}",
194+
reason
195+
)));
196+
return Some(tui.current_mode);
197+
}
193198
}
194199

195200
// Save UI state before switching (for full-screen mode persistence)

packages/nx/src/native/tui/tui.rs

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crossterm::{
1010
use futures::{FutureExt, StreamExt};
1111
use ratatui::backend::CrosstermBackend;
1212
use std::{
13+
env,
1314
io::{IsTerminal, Write},
1415
ops::{Deref, DerefMut},
1516
time::Duration,
@@ -86,10 +87,10 @@ impl Tui {
8687
// escape sequences that wait for responses on stdin. In git hooks and other
8788
// non-interactive environments, stdin is redirected so these queries hang.
8889
// By checking upfront, we avoid flakiness from deferred initialization.
89-
let inline_terminal = if Self::is_stdin_interactive() {
90-
let size = crossterm::terminal::size();
91-
debug!("Terminal size: {:?}", size);
92-
let inline_height = size.map(|(_cols, rows)| rows).unwrap_or(24);
90+
let inline_terminal = if Self::inline_viewport_supported() {
91+
let inline_height = crossterm::terminal::size()
92+
.map(|(_cols, rows)| rows)
93+
.unwrap_or(24);
9394
ratatui::Terminal::with_options(
9495
CrosstermBackend::new(std::io::stderr()),
9596
ratatui::TerminalOptions {
@@ -135,10 +136,35 @@ impl Tui {
135136
/// which hang indefinitely if stdin isn't a real terminal.
136137
///
137138
/// See: https://github.com/crossterm-rs/crossterm/issues/692
138-
pub fn is_stdin_interactive() -> bool {
139+
fn is_stdin_interactive() -> bool {
139140
std::io::stdin().is_terminal()
140141
}
141142

143+
fn is_inline_mode_disabled() -> bool {
144+
let env = env::var("NX_TUI_INLINE_MODE");
145+
match env {
146+
Ok(val) if val == "0" || val.to_lowercase() == "false" => true,
147+
_ => false,
148+
}
149+
}
150+
151+
fn inline_viewport_supported() -> bool {
152+
Self::is_stdin_interactive() && !Self::is_inline_mode_disabled()
153+
}
154+
155+
pub fn inline_tui_unsupported_reason(&self) -> Option<String> {
156+
if !Self::is_stdin_interactive() {
157+
return Some("Stdin is not a TTY".to_string());
158+
}
159+
if Self::is_inline_mode_disabled() {
160+
return Some("NX_TUI_INLINE_MODE is set to false or 0".to_string());
161+
}
162+
if !self.inline_terminal.is_some() {
163+
return Some("Inline terminal failed to initialize".to_string());
164+
}
165+
None
166+
}
167+
142168
/// Returns a reference to the currently active terminal based on mode
143169
pub fn terminal(&self) -> &ratatui::Terminal<Backend> {
144170
match self.current_mode {

0 commit comments

Comments
 (0)