Skip to content

Conversation

@jdx
Copy link
Owner

@jdx jdx commented Oct 26, 2025

Overview

This PR significantly improves the robustness of confirm and input dialogs in non-interactive environments, fixes multiple input parsing issues, and enhances the developer experience with better testing tools.

Problems Solved

1. Terminal Noise in Non-TTY Environments

When stderr is not a TTY (e.g., in CI), rendering interactive prompts with terminal control sequences generates massive noise from cursor movement, colors, and escape codes. This was causing issues in fnox CI where Bitwarden CLI (which uses demand) would flood logs with hundreds of lines of escape sequences.

2. Input Parsing Ignores Custom Labels

The confirm dialog accepted hardcoded "y"/"yes"/"n"/"no" regardless of custom affirmative/negative labels, creating confusing behavior where users could enter "yes" even when the prompt displayed "Confirm/Cancel [c/c]".

3. Ambiguous Key Suggestions

When affirmative and negative labels share the same starting character (e.g., "Confirm" and "Cancel"), the dialog would:

  • Show ambiguous prompt hints like [c/c]
  • Always resolve to affirmative when user typed the shared character
  • Make it impossible to select the negative option in non-TTY mode

4. Panics on Empty Label Strings

When custom affirmative/negative strings were empty, the code would panic trying to get the first character.

Solutions

TTY Detection & Fallback (src/tty.rs)

  • Added new module with helper functions for TTY detection, prompt writing, and input reading
  • Updated Input::run and Confirm::run to check if both stdin AND stderr are terminals before entering interactive mode
  • Implemented simple text prompt fallbacks (without terminal control) for non-TTY environments
  • Prompts write to stderr and read responses from stdin

Input Parsing Improvements (src/confirm.rs)

Custom Label Support:

  • Input parsing now respects custom affirmative/negative labels instead of hardcoded "y/yes/n/no"
  • Accepts first character of label (e.g., "c" for "Confirm")
  • Accepts partial matches (e.g., "conf" for "Confirm")
  • Accepts full label text (e.g., "confirm" for "Confirm")

Ambiguous Key Resolution:

  • Added calculate_hints() method to detect when first characters conflict
  • Calculates minimum unique prefixes (e.g., "co" for Confirm, "ca" for Cancel)
  • Non-TTY mode requires unique prefix to disambiguate
  • TTY mode disables single-key shortcuts when ambiguous (arrow keys + enter only)
  • Shows clear "Ambiguous input" error with required prefixes
  • Prompts display unique prefixes: [co/ca] instead of [c/c]

Panic Prevention:

  • Added unwrap_or('y') and unwrap_or('n') fallbacks when getting first characters from labels

Test Infrastructure Improvements

Integration Tests (tests/non_tty_input.rs):

  • Non-TTY behavior verification
  • Prompt visibility checks
  • Custom label input parsing (first char, full word, partial match)
  • Rejection of hardcoded y/yes when using custom labels
  • Ambiguous input handling and unique prefix validation
  • Windows line ending handling
  • Validation behavior

Unit Tests (src/confirm.rs):

  • Hint calculation with/without conflicts
  • Render output verification
  • Edge cases like identical prefixes ("Complete"/"Completed")

Developer Experience:

  • Converted render tests to use insta inline snapshots
  • Added examples/confirm_custom.rs demonstrating custom labels
  • Snapshots stored inline with readable formatting
  • Easy updates with cargo insta accept

Tooling & CI

  • Added hk.pkl configuration for consistent formatting/linting
  • Updated CI workflow to use hk check --all
  • Added mise.toml configuration for hk
  • Added insta dev dependency for snapshot testing

Testing

All existing tests continue to pass plus:

Files Changed

New Files:

  • src/tty.rs - TTY detection and fallback prompt helpers
  • examples/confirm_custom.rs - Custom label example (Confirm/Cancel)
  • hk.pkl - hk configuration

Modified Files:

  • src/confirm.rs - TTY detection, custom label parsing, ambiguous key handling, panic prevention, tests (199 lines added)
  • src/input.rs - TTY detection integration (29 lines)
  • tests/non_tty_input.rs - Comprehensive integration tests (272 lines, 187 net added)
  • Cargo.toml - Added insta = "1" dev dependency
  • .github/workflows/test.yml - Use hk for CI checks
  • mise.toml - Add hk configuration
  • src/lib.rs - Export tty module

Breaking Changes

None. All changes are backwards compatible. Existing code using default labels ("Yes"/"No") continues to work exactly as before.


Note

Adds non-TTY prompt fallbacks and refined confirm input parsing (with unique-prefix hints) plus new tests, example, and CI/tooling updates.

  • Core (non-TTY support)
    • Add src/tty.rs with is_tty, write_prompt, read_line; use in Input::run and Confirm::run to avoid escape codes and read stdin in non-interactive environments.
  • Confirm dialog
    • Implement calculate_hints() to derive shortcuts and minimum unique prefixes; prevent panics on empty labels.
    • Respect custom labels in parsing (prefix/word matches); handle ambiguous first chars by disabling single-key shortcuts (TTY) and requiring unique prefixes (non-TTY).
    • Update help hints to show {affirmative}/{negative}/enter or enter when ambiguous.
  • Input
    • Use non-TTY prompt fallback; preserve validation behavior.
  • Tests
    • Add integration tests in tests/non_tty_input.rs for non-TTY behavior, validation, Windows line endings, confirm custom-label parsing, ambiguity, and prompt visibility.
    • Convert confirm render tests to insta snapshots and add unit tests for hint calculation and custom-label rendering.
  • Examples
    • Add examples/confirm_custom.rs demonstrating custom labels.
  • Tooling/CI
    • Add hk.pkl and mise.toml; update workflow to use jdx/mise-action@v3 and hk check --all.
    • Add insta to dev-dependencies.

Written by Cursor Bugbot for commit d741e8c. This will update automatically on new commits. Configure here.

When stderr is not a TTY (e.g., in CI environments), attempting to render
interactive prompts with terminal control sequences generates massive noise
from cursor movement, colors, and other escape codes.

This change updates Input and Confirm to:
1. Check if both stdin AND stderr are terminals before entering interactive mode
2. Fall back to simple text prompts (without terminal control) when not in a TTY
3. Write prompts to stderr and read responses from stdin

This fixes the issue where programs using demand (like Bitwarden CLI) would
flood CI logs with hundreds of lines of escape sequences when trying to
prompt for passwords in non-interactive environments.

The simple fallback prompts still work correctly, they just don't use any
terminal control sequences that would create noise in logs.
cursor[bot]

This comment was marked as outdated.

jdx added 3 commits October 26, 2025 12:34
Created a new tty module with shared helpers:
- is_tty(): Check if both stdin and stderr are terminals
- write_prompt(): Write simple text prompts to stderr
- read_line(): Read and strip line endings from stdin

This eliminates ~30 lines of duplicated code between input.rs and
confirm.rs, making the non-TTY handling more maintainable and consistent
across different prompt types.
@jdx jdx requested a review from roele October 26, 2025 17:40
cursor[bot]

This comment was marked as outdated.

Replaced unwrap() with unwrap_or() to provide default characters:
- Confirm: 'y' for affirmative, 'n' for negative
- DialogButton: 'x' for empty labels

This fixes panics when users set empty strings via the public API:
- Confirm::new("title").affirmative("")
- DialogButton::new("")

The fix applies to all code paths (TTY and non-TTY).
@jdx jdx force-pushed the fix/non-tty-check branch 5 times, most recently from 8c55274 to e2b9279 Compare October 26, 2025 17:48
@jdx jdx force-pushed the fix/non-tty-check branch from e2b9279 to 6a2b50c Compare October 26, 2025 17:50
cursor[bot]

This comment was marked as outdated.

Previously, the confirm dialog would accept hardcoded "y"/"yes"/"n"/"no"
responses regardless of custom labels. This created confusing behavior where
users could enter "yes" even when the prompt displayed custom labels like
"Confirm/Cancel" with "c/c" as the expected characters.

Now the input parsing only accepts responses that match the custom labels:
- First character of the label (e.g., "c" for "Confirm")
- Partial matches (e.g., "conf" for "Confirm")
- Full label text (e.g., "confirm" for "Confirm")

Added comprehensive tests including new example and integration tests to
verify the fix works correctly in non-TTY mode.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
cursor[bot]

This comment was marked as outdated.

@jdx jdx changed the title fix: check stderr is TTY before using terminal control sequences fix: improve non-TTY handling and input parsing for confirm/input dialogs Oct 26, 2025
When affirmative and negative labels start with the same character (e.g.,
"Confirm" and "Cancel"), the previous implementation would:
1. Show ambiguous prompt hints like [c/c]
2. Always resolve to affirmative when user typed the shared character
3. Make it impossible to select the negative option in non-TTY mode

Changes:
- Added calculate_hints() method to find minimum unique prefixes
- When first characters conflict, finds shortest unique prefix (e.g., "co"/"ca")
- Non-TTY mode now requires unique prefix to disambiguate
- TTY mode disables single-key shortcuts when ambiguous (arrow keys + enter only)
- Shows clear "Ambiguous input" error with required prefixes
- Updated prompts to display unique prefixes: [co/ca] instead of [c/c]

Added comprehensive tests:
- Unit tests for hint calculation with/without conflicts
- Integration tests for ambiguous input handling
- Tests for unique prefix acceptance (co/ca)
- Verification that prompt displays correct hints

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
cursor[bot]

This comment was marked as outdated.

Updated test expectations to match actual rendered output:
- Added trailing spaces after negative button (from button padding)
- Added extra newline for tests without description
- Fixed lifetime issues by storing rendered string before calling without_ansi
- Removed unused indoc import

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
cursor[bot]

This comment was marked as outdated.

@jdx jdx changed the title fix: improve non-TTY handling and input parsing for confirm/input dialogs fix: improve non-TTY support, input parsing, and ambiguous key handling for confirm dialog Oct 26, 2025
Benefits:
- Easier to maintain: snapshots are stored inline with readable formatting
- Clear diffs: changes to rendered output are immediately visible
- Auto-update: cargo insta accept automatically updates snapshots
- Better readability: raw strings preserve whitespace and special characters

Changes:
- Added insta = "1" as dev dependency
- Converted test_render tests to use assert_snapshot! macro
- Snapshots now stored inline with @r"..." syntax
- Trailing spaces and formatting automatically captured

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@jdx jdx force-pushed the fix/non-tty-check branch from 47a2582 to d2e977b Compare October 26, 2025 18:06
@jdx
Copy link
Owner Author

jdx commented Oct 26, 2025

Dang, I think I need to make hk work on windows

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.

2 participants