Skip to content

fix(timer): restore Ctrl+C cancellation while the timer runs - #642

Merged
aversini merged 1 commit into
mainfrom
fix_timer_restore_ctrl_c_cancellation
Aug 8, 2026
Merged

aversini merged 1 commit into
mainfrom
fix_timer_restore_ctrl_c_cancellation

Conversation

@aversini

@aversini aversini commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Problem

Ctrl+C stopped cancelling a running timer — the process had to be killed from another shell.

Root cause

Upstream, in stdin-discarder@0.3.2 by way of ora@9.4.1 — not in our code.

When the spinner starts, ora puts stdin in raw mode. Raw mode stops the tty from turning Ctrl+C into SIGINT and delivers a bare 0x03 byte instead. stdin-discarder means to read that byte and re-send the signal itself, but it cannot:

  • it attaches its reader with prependListener("data", …), which — unlike on("data") — does not start a paused stream flowing;
  • it only calls resume() when stdin.isPaused() was true beforehand. On a fresh process.stdin, flowing is null, so isPaused() returns false and the resume never happens.

The byte is never read, the signal is never re-sent, and the tty's own SIGINT is disabled. Ctrl+C is swallowed for the whole life of the spinner. Instrumentation shows it directly: readableFlowing=null with data listeners=1 — a reader attached to a stream that never flows.

Fix

One line, plus a comment so it doesn't get "cleaned up" later:

const spinner = new Spinner({ discardStdin: false });

ora then leaves the terminal in canonical mode and the kernel delivers a real SIGINT. Spinner already forwarded ora options, so @node-cli/logger needed no change. There is nothing to bump to instead — ora@9.4.1 and stdin-discarder@0.3.2 are both current.

Verification

Driven under a real pty, since ora only sets raw mode when isTTY:

Result
Released 2.1.1 still alive 5s after Ctrl+C
This branch exits in ~30ms, signal=2
  • Interrupted at 0.3s, 1.0s, 2.0s and 3.5s into the timer — all cancel cleanly
  • Cursor restored (ESC[?25l → ESC[?25h) and spinner line cleared on the way out
  • Expiry path intact: ✔ Time's up!, exit 0
  • 15/15 timer tests; full suite green across 12 projects; comments reports unchanged

Tradeoff

Keystrokes typed while the spinner runs are now echoed — that is what discardStdin suppressed. Ordinary CLI behavior, and the lesser cost against a timer that cannot be cancelled. Keeping the discard is not possible without the raw mode that causes the bug.

Note

3 pre-existing lint warnings in src/__tests__/utilities.test.ts (unused imports) are untouched — separate PR if wanted.

🤖 Generated with Claude Code

https://claude.ai/code/session_01T5ssXq3kB2gv4wDANqjogQ

Ctrl+C no longer stopped a running timer; the process had to be killed
from another shell. The cause is upstream, in stdin-discarder@0.3.2 by
way of ora@9.4.1.

When the spinner starts, ora puts stdin in raw mode. Raw mode stops the
tty from turning Ctrl+C into SIGINT and delivers a bare 0x03 byte
instead. stdin-discarder means to read that byte and re-send the signal
itself, but it cannot: it attaches its reader with prependListener,
which (unlike on("data")) does not start a paused stream flowing, and it
only calls resume() when stdin.isPaused() was true beforehand. On a
fresh process.stdin, flowing is null, so isPaused() is false and the
resume never happens. The byte is never read, the signal is never
re-sent, and the tty's own SIGINT is disabled: Ctrl+C is swallowed for
the whole life of the spinner.

Turning discardStdin off leaves the terminal in canonical mode, so the
kernel delivers a real SIGINT again. Spinner already forwarded ora
options, so @node-cli/logger needed no change. There is nothing to bump
to instead: ora 9.4.1 and stdin-discarder 0.3.2 are both current.

Verified under a real pty, since ora only sets raw mode when isTTY:
released 2.1.1 is still alive 5s after Ctrl+C, while the patched build
exits in ~30ms with signal 2 when interrupted at 0.3s, 1.0s, 2.0s and
3.5s. The cursor is restored and the spinner line cleared on the way
out, and the expiry path still prints "Time's up!" and exits 0.

Tradeoff: keystrokes typed while the spinner runs are now echoed, which
is what discardStdin suppressed. That is ordinary CLI behavior and is
the lesser cost against a timer that cannot be cancelled; keeping the
discard is not possible without the raw mode that causes the bug.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T5ssXq3kB2gv4wDANqjogQ
@aversini
aversini merged commit 36ae6e2 into main Aug 8, 2026
5 checks passed
@aversini
aversini deleted the fix_timer_restore_ctrl_c_cancellation branch August 8, 2026 22:25
@aversini aversini mentioned this pull request Aug 8, 2026
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