fix(ui): clear spinner line after joining its thread

Clearing before the join left a race in the normal path: a spinner thread
sitting just past its stop_event.is_set() check could repaint the line after
the erase, leaving the terminal dirty. Join first so no further writes are
possible, and nest the erase in a finally so an interrupted join (hate_crack
raises DoubleInterrupt on a second SIGINT) still leaves a clean line.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Justin Bollinger
2026-07-24 18:13:23 -04:00
co-authored by Claude
parent 142c1fc99f
commit 86fac864a0
+10 -9
View File
@@ -59,12 +59,13 @@ def spinner(message: str) -> "Generator[None, None, None]":
yield
finally:
stop_event.set()
# Clear the spinner line *before* joining so the terminal is left clean
# even if join() is interrupted by a second Ctrl-C (DoubleInterrupt).
# hate_crack's _sigint_handler raises DoubleInterrupt on a second SIGINT
# within 2 s, and join() can block up to _TICK_INTERVAL (0.12 s) — a
# narrow but real window. Writing the escape sequence first ensures the
# line is always erased regardless of what happens to the join.
sys.stdout.write("\033[2K\r")
sys.stdout.flush()
thread.join()
# Clear *after* the join, so a thread sitting just past its is_set()
# check cannot repaint the line after we erase it. The nested finally
# keeps that guarantee even when join() is interrupted: hate_crack's
# _sigint_handler raises DoubleInterrupt on a second SIGINT within 2 s,
# and join() can block up to _TICK_INTERVAL (0.12 s).
try:
thread.join()
finally:
sys.stdout.write("\033[2K\r")
sys.stdout.flush()