Skip to content

fix(cli): keel tui fails gracefully without an interactive terminal - #141

Merged
eaitbrahim merged 1 commit into
mainfrom
feat/tui-tty-guard
Jul 24, 2026
Merged

fix(cli): keel tui fails gracefully without an interactive terminal#141
eaitbrahim merged 1 commit into
mainfrom
feat/tui-tty-guard

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

What

Follow-up to #140. Running keel tui (the live, full-screen path) without a real controlling TTY dumped a raw traceback:

_curses.error: cbreak() returned ERR

This surfaced when launching it through a pipe / a harness that captures stdio rather than owning a terminal. Now it fails with a clean, actionable message instead.

Fix

  • Pre-check _stdio_is_interactive() (both stdin and stdout are TTYs) in tui_cmd before entering curses; if not, raise a ClickException pointing the user at keel tui --once.
  • Belt-and-braces: run_live also catches curses.error raised by curses.wrapper itself — the case where a terminal passes isatty() but still can't be put into cbreak mode — and turns it into the same helpful message.

--once (the non-curses snapshot path, used by pipes/CI) is unaffected.

Testing

  • +3 tests (_stdio_is_interactive both-TTY logic; keel tui under CliRunner exits cleanly with a --once hint; run_live wraps a curses.error from wrapper as a ClickException).
  • Full suite 1522 passed, ruff check clean.
  • Verified: piped keel tui now prints Error: keel tui needs an interactive terminal … use \keel tui --once`instead of a traceback;--once` still renders a snapshot.

🤖 Generated with Claude Code

Running `keel tui` (the live path) without a real controlling TTY dumped a
raw `_curses.error: cbreak() returned ERR` traceback — hit when it's run
through a pipe or a harness that captures stdio instead of a terminal.

- Pre-check `_stdio_is_interactive()` (both stdin AND stdout are TTYs) in
  `tui_cmd` before entering curses; if not, raise a clean `ClickException`
  pointing at `keel tui --once`.
- Belt-and-braces: `run_live` now also catches `curses.error` from
  `curses.wrapper` itself (a TTY that passes `isatty()` yet can't be put
  into cbreak mode) and turns it into the same actionable message.

`--once` (the non-curses snapshot path) is unaffected. +3 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@eaitbrahim
eaitbrahim merged commit 89a3501 into main Jul 24, 2026
1 check passed
@eaitbrahim
eaitbrahim deleted the feat/tui-tty-guard branch July 24, 2026 09:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant