From 28fa7f7734a339ac3fd6d92d184607b9c2e8d54f Mon Sep 17 00:00:00 2001 From: Ahmed Allam Date: Sun, 4 Oct 2026 17:34:55 +0000 Subject: [PATCH] fix(resume-picker): treat ctrl+c as cancel instead of leaking a KeyboardInterrupt traceback --- strix/interface/resume_picker.py | 8 ++++++-- tests/test_resume_picker.py | 22 +++++++++++++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/strix/interface/resume_picker.py b/strix/interface/resume_picker.py index 47e797e3..13c98de9 100644 --- a/strix/interface/resume_picker.py +++ b/strix/interface/resume_picker.py @@ -331,7 +331,7 @@ class ResumePicker: """Apply one key: (done, run), where done with no run means cancelled.""" self.notice = "" if key == KEY_INTERRUPT: - raise KeyboardInterrupt + return True, None if key == KEY_ESCAPE: if not self.filter: return True, None @@ -380,7 +380,11 @@ class ResumePicker: try: self.draw() while True: - done, chosen = self.handle(next_key()) + try: + key = next_key() + except KeyboardInterrupt: + key = KEY_INTERRUPT + done, chosen = self.handle(key) if done: return chosen self.draw() diff --git a/tests/test_resume_picker.py b/tests/test_resume_picker.py index e3082e73..f86bd44d 100644 --- a/tests/test_resume_picker.py +++ b/tests/test_resume_picker.py @@ -182,11 +182,6 @@ def test_no_match_renders_a_hint_and_enter_does_nothing() -> None: assert picker.handle(KEY_ENTER) == (False, None) -def test_ctrl_c_raises_keyboard_interrupt() -> None: - with pytest.raises(KeyboardInterrupt): - _picker().handle(KEY_INTERRUPT) - - def test_long_lists_scroll_to_keep_the_cursor_visible() -> None: runs = [ _run(f"run_{index:02d}", f"https://host{index}.example", minutes_ago=index) @@ -218,6 +213,23 @@ def test_run_redraws_in_place_and_restores_the_cursor() -> None: assert "example-com_223e" in output +def test_ctrl_c_cancels_like_escape() -> None: + console, _ = _console() + picker = ResumePicker(RUNS, console=console, runs_dir="strix_runs", now=NOW) + assert picker.handle(KEY_INTERRUPT) == (True, None) + + +def test_sigint_while_waiting_for_a_key_cancels_and_restores_the_cursor() -> None: + console, buffer = _console() + picker = ResumePicker(RUNS, console=console, runs_dir="strix_runs", now=NOW) + + def interrupted() -> str: + raise KeyboardInterrupt + + assert picker.run(interrupted) is None + assert buffer.getvalue().endswith("\x1b[?25h") + + def test_pick_run_needs_a_terminal() -> None: console, _ = _console() with pytest.raises(PickerUnavailableError):