From b266a0f5d456595e730c8d42f4a454b0766d6779 Mon Sep 17 00:00:00 2001 From: Rayan-and-beyond <263488867+Rayan-and-beyond@users.noreply.github.com> Date: Mon, 14 Sep 2026 01:51:30 +0300 Subject: [PATCH] fix: reject non-interactive tui Signed-off-by: Rayan-and-beyond <263488867+Rayan-and-beyond@users.noreply.github.com> --- CHANGELOG.md | 4 ++++ crates/oxidelake-runtime/src/bin/oxide.rs | 4 ++++ crates/oxidelake-runtime/tests/cli.rs | 9 +++++++++ crates/oxidelake-tui/src/app.rs | 16 +++++++++++++++- crates/oxidelake-tui/src/lib.rs | 5 ++++- 5 files changed, 36 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca76d3c..cf6f983 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,10 @@ versions (0.x) may contain breaking changes; they are always listed under a ### Fixed +- **`oxide tui` now rejects non-interactive stdin/stdout before terminal setup.** + It prints a clear hint to use `oxide sql` or `oxide explain` for scripted output + and exits with usage code 2 instead of leaking a raw ENXIO-style terminal error. + - **The dashboard's Describe panel no longer prints a truncated percentile as if it were the value.** `approx_percentile_cont` renders full precision, and the panel's six-character columns were clipped from the right, so the 2M-row diff --git a/crates/oxidelake-runtime/src/bin/oxide.rs b/crates/oxidelake-runtime/src/bin/oxide.rs index 986678a..12dc0d7 100644 --- a/crates/oxidelake-runtime/src/bin/oxide.rs +++ b/crates/oxidelake-runtime/src/bin/oxide.rs @@ -157,6 +157,10 @@ async fn main() -> anyhow::Result<()> { tables, target, } => { + if !oxidelake_tui::is_interactive_terminal() { + eprintln!("{}", oxidelake_tui::NON_INTERACTIVE_TERMINAL_MESSAGE); + std::process::exit(2); + } let model = match &query { Some(sql) => { let session = session(None, target, &tables).await?; diff --git a/crates/oxidelake-runtime/tests/cli.rs b/crates/oxidelake-runtime/tests/cli.rs index 54f8c38..075b0f7 100644 --- a/crates/oxidelake-runtime/tests/cli.rs +++ b/crates/oxidelake-runtime/tests/cli.rs @@ -24,6 +24,15 @@ fn oxide() -> Command { Command::cargo_bin("oxide").unwrap() } +#[test] +fn tui_rejects_non_interactive_terminal() { + oxide() + .arg("tui") + .assert() + .code(2) + .stderr("oxide tui needs an interactive terminal; use oxide sql or oxide explain for non-interactive output\n"); +} + fn gen_data(dir: &Path, rows: u64) { let assert = oxide() .args([ diff --git a/crates/oxidelake-tui/src/app.rs b/crates/oxidelake-tui/src/app.rs index 51bfdcf..cc71529 100644 --- a/crates/oxidelake-tui/src/app.rs +++ b/crates/oxidelake-tui/src/app.rs @@ -1,6 +1,6 @@ //! The real-terminal event loop (crossterm) with DEC 2026 synchronized updates. -use std::io::{self, Write}; +use std::io::{self, IsTerminal, Write}; use std::time::Duration; use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; @@ -12,6 +12,14 @@ use crate::model::DashboardModel; use crate::render::render; use crate::state::{AppState, KeyInput, Transition}; +/// Message shown when the dashboard is started without an interactive terminal. +pub const NON_INTERACTIVE_TERMINAL_MESSAGE: &str = "oxide tui needs an interactive terminal; use oxide sql or oxide explain for non-interactive output"; + +/// Whether both input and output are attached to an interactive terminal. +pub fn is_interactive_terminal() -> bool { + io::stdin().is_terminal() && io::stdout().is_terminal() +} + /// Maps a crossterm key event onto the dashboard's input alphabet. Key /// releases (reported by some terminals) are ignored. pub fn key_input_from_crossterm(key: &KeyEvent) -> Option { @@ -52,6 +60,12 @@ pub fn run_terminal( tick: Option, mut refresh: impl FnMut(&mut DashboardModel), ) -> io::Result<()> { + if !is_interactive_terminal() { + return Err(io::Error::new( + io::ErrorKind::NotConnected, + NON_INTERACTIVE_TERMINAL_MESSAGE, + )); + } let mut terminal = ratatui::try_init()?; let mut state = AppState::new(model.plan().len()); let result = (|| -> io::Result<()> { diff --git a/crates/oxidelake-tui/src/lib.rs b/crates/oxidelake-tui/src/lib.rs index 6465d59..789d01c 100644 --- a/crates/oxidelake-tui/src/lib.rs +++ b/crates/oxidelake-tui/src/lib.rs @@ -20,7 +20,10 @@ pub mod model; pub mod render; pub mod state; -pub use app::{draw_frame, key_input_from_crossterm, run_terminal}; +pub use app::{ + NON_INTERACTIVE_TERMINAL_MESSAGE, draw_frame, is_interactive_terminal, + key_input_from_crossterm, run_terminal, +}; pub use model::{ColumnProfile, DashboardModel, demo_model}; pub use render::render; pub use state::{AppState, KeyInput, Panel, Transition};