From 3957e8622329b78f58c20fe23312ab21a38d3188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kl=C3=B6tzke?= Date: Mon, 19 Jan 2026 20:40:00 +0100 Subject: [PATCH] tty: restore cursor when stopped If the user presses Ctrl+Z on the terminal, the kernel will send SIGTSTP to the current foreground process group. By default, this will directly stop all processes. But if we have messed with the terminal (e.g. disabled cursor), this will leave the terminal in a barely usable state. Fix that by handling SIGTSTP and restore the original tty settings. If that is done, we can stop ourself. Upon resumption, re-initialize the tty and carry on. --- pym/bob/tty.py | 55 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/pym/bob/tty.py b/pym/bob/tty.py index 31c9f9ff..88b575e0 100644 --- a/pym/bob/tty.py +++ b/pym/bob/tty.py @@ -126,6 +126,14 @@ def setVerbosity(self, verbosity): def cleanup(self): pass + def suspend(self): + """Restore tty settings upon SIGTSTP (terminal stop)""" + self.cleanup() + + def resume(self): + """Resume tty after SIGTSTP""" + pass + def setProgress(self, done, num): pass @@ -250,6 +258,9 @@ def __init__(self, verbosity, maxJobs): self.__tasksDone = 0 self.__tasksNum = 1 + self.__ttyInit() + + def __ttyInit(self): # disable cursor print("\x1b[?25l") @@ -353,6 +364,10 @@ def cleanup(self): except ImportError: pass + def resume(self): + self.__ttyInit() + self.__putFooter() + def setProgress(self, done, num): self.__tasksDone = done self.__tasksNum = num @@ -451,6 +466,9 @@ def __init__(self, verbosity, maxJobs): self.__tasksDone = 0 self.__tasksNum = 1 + self.__ttyInit() + + def __ttyInit(self): # disable cursor print("\x1b[?25l") @@ -541,6 +559,10 @@ def cleanup(self): except ImportError: pass + def resume(self): + self.__ttyInit() + self.__putFooter() + def setProgress(self, done, num): self.__tasksDone = done self.__tasksNum = num @@ -592,6 +614,11 @@ def ttyReinit(): if __onTTY and sys.platform == "win32": kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), __origMode.value | 4) +def handleTerminalStop(signum, frame): + __tui.suspend() + signal.raise_signal(signal.SIGSTOP) + __tui.resume() + # module initialization __onTTY = (sys.stdout.isatty() and sys.stderr.isatty()) @@ -599,17 +626,23 @@ def ttyReinit(): __tui = SingleTUI(NORMAL) __parallelTUIThreshold = 16 -if __onTTY and sys.platform == "win32": - # Try to set ENABLE_VIRTUAL_TERMINAL_PROCESSING flag. Enables vt100 color - # codes on Windows 10 console. If this fails we inhibit color code usage - # because it will clutter the output. - import ctypes - import ctypes.wintypes - __origMode = ctypes.wintypes.DWORD() - kernel32 = ctypes.windll.kernel32 - kernel32.GetConsoleMode(kernel32.GetStdHandle(-11), ctypes.byref(__origMode)) - if not kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), __origMode.value | 4): - __onTTY = False +if __onTTY: + if sys.platform == "win32": + # Try to set ENABLE_VIRTUAL_TERMINAL_PROCESSING flag. Enables vt100 color + # codes on Windows 10 console. If this fails we inhibit color code usage + # because it will clutter the output. + import ctypes + import ctypes.wintypes + __origMode = ctypes.wintypes.DWORD() + kernel32 = ctypes.windll.kernel32 + kernel32.GetConsoleMode(kernel32.GetStdHandle(-11), ctypes.byref(__origMode)) + if not kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), __origMode.value | 4): + __onTTY = False + else: + # Intercept SIGTSTP to leave TTY in a sane state if user presses Ctrl+Z. + import signal + signal.signal(signal.SIGTSTP, handleTerminalStop) + def setColorMode(mode): global __useColor