Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@

### Fixed

- hush: TUI now reports a clear "no TTY" error with subcommand guidance instead of crossterm's cryptic "Device not configured" failure (#57)
- dependencies: refresh ip-address, brace-expansion, protobufjs, and the docs starter security lockfiles; retain protobufjs 7.x compatibility (#56)
- remediate merged-#35 CodeRabbit findings (#36) [#35]
- post-merge remediation for #33 (validator hardening, packaging hygiene, ask-codex metadata) (#34) [#33]
Expand Down
21 changes: 19 additions & 2 deletions bws-tui/src/tui/terminal.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use super::{events, App};
use anyhow::{anyhow, Context, Result};
use anyhow::{anyhow, bail, Context, Result};
use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::io::{self, Write};
use std::io::{self, IsTerminal, Write};

#[derive(Default)]
struct TerminalGuard {
Expand Down Expand Up @@ -49,7 +49,16 @@ impl Drop for TerminalGuard {
}
}

/// Reject headless sessions before changing terminal state, then run the TUI
/// and report event-loop failures together with terminal restoration failures.
pub(super) fn run(app: &mut App) -> Result<()> {
if !is_interactive(io::stdin().is_terminal(), io::stdout().is_terminal()) {
bail!(
"hush's interactive TUI needs a terminal session (no TTY found). \
Run `hush` from an interactive terminal, or use the script-friendly \
subcommands: `hush list`, `hush get --key <KEY>`, `hush exec --key <KEY> -- <cmd>`"
);
}
let mut guard = TerminalGuard::default();
guard.enable_raw()?;
let mut output = io::stdout();
Expand All @@ -60,6 +69,14 @@ pub(super) fn run(app: &mut App) -> Result<()> {
finish_terminal(event, raw, screen)
}

/// The TUI needs either side attached to a terminal; crossterm opens /dev/tty
/// directly for input when stdin is piped, so stdout alone is enough.
pub(super) fn is_interactive(stdin_tty: bool, stdout_tty: bool) -> bool {
stdin_tty || stdout_tty
}

/// Preserve every event-loop and restoration error rather than masking one
/// failure with a later cleanup failure.
pub(super) fn finish_terminal(
event: Result<()>,
raw: Result<()>,
Expand Down
10 changes: 10 additions & 0 deletions bws-tui/src/tui/tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::*;
use crate::tui::terminal::is_interactive;
use anyhow::anyhow;

#[test]
Expand Down Expand Up @@ -52,6 +53,15 @@ fn edit_fields_cycle_without_invalid_numeric_states() {
assert_eq!(EditField::Key.previous(), EditField::Note);
}

#[test]
/// Cover both one-sided terminal sessions and the fully headless rejection.
fn tui_gate_accepts_a_tty_on_either_side() {
assert!(is_interactive(true, false));
assert!(is_interactive(false, true));
assert!(is_interactive(true, true));
assert!(!is_interactive(false, false));
}

#[test]
fn terminal_cleanup_reports_every_failure() {
let error = terminal::finish_terminal(
Expand Down
Loading