|
6 | 6 | import platform |
7 | 7 | import subprocess |
8 | 8 | import sys |
| 9 | +from typing import Callable, Optional |
| 10 | + |
| 11 | +# Store the original console ctrl handler so we can restore it if needed |
| 12 | +_original_ctrl_handler: Optional[Callable] = None |
9 | 13 |
|
10 | 14 |
|
11 | 15 | def reset_windows_terminal_ansi() -> None: |
@@ -86,17 +90,36 @@ def reset_windows_console_mode() -> None: |
86 | 90 | pass # Silently ignore errors - best effort reset |
87 | 91 |
|
88 | 92 |
|
| 93 | +def flush_windows_keyboard_buffer() -> None: |
| 94 | + """Flush the Windows keyboard buffer. |
| 95 | +
|
| 96 | + Clears any pending keyboard input that could interfere with |
| 97 | + subsequent input operations after an interrupt. |
| 98 | + """ |
| 99 | + if platform.system() != "Windows": |
| 100 | + return |
| 101 | + |
| 102 | + try: |
| 103 | + import msvcrt |
| 104 | + |
| 105 | + while msvcrt.kbhit(): |
| 106 | + msvcrt.getch() |
| 107 | + except Exception: |
| 108 | + pass # Silently ignore errors - best effort flush |
| 109 | + |
| 110 | + |
89 | 111 | def reset_windows_terminal_full() -> None: |
90 | | - """Perform a full Windows terminal reset (ANSI + console mode). |
| 112 | + """Perform a full Windows terminal reset (ANSI + console mode + keyboard buffer). |
91 | 113 |
|
92 | | - Combines both ANSI reset and console mode reset for complete |
93 | | - terminal state restoration after interrupts. |
| 114 | + Combines ANSI reset, console mode reset, and keyboard buffer flush |
| 115 | + for complete terminal state restoration after interrupts. |
94 | 116 | """ |
95 | 117 | if platform.system() != "Windows": |
96 | 118 | return |
97 | 119 |
|
98 | 120 | reset_windows_terminal_ansi() |
99 | 121 | reset_windows_console_mode() |
| 122 | + flush_windows_keyboard_buffer() |
100 | 123 |
|
101 | 124 |
|
102 | 125 | def reset_unix_terminal() -> None: |
@@ -124,3 +147,145 @@ def reset_terminal() -> None: |
124 | 147 | reset_windows_terminal_full() |
125 | 148 | else: |
126 | 149 | reset_unix_terminal() |
| 150 | + |
| 151 | + |
| 152 | +def disable_windows_ctrl_c() -> bool: |
| 153 | + """Disable Ctrl+C processing at the Windows console input level. |
| 154 | +
|
| 155 | + This removes ENABLE_PROCESSED_INPUT from stdin, which prevents |
| 156 | + Ctrl+C from being interpreted as a signal at all. Instead, it |
| 157 | + becomes just a regular character (^C) that gets ignored. |
| 158 | +
|
| 159 | + This is more reliable than SetConsoleCtrlHandler because it |
| 160 | + prevents Ctrl+C from being processed before it reaches any handler. |
| 161 | +
|
| 162 | + Returns: |
| 163 | + True if successfully disabled, False otherwise. |
| 164 | + """ |
| 165 | + global _original_ctrl_handler |
| 166 | + |
| 167 | + if platform.system() != "Windows": |
| 168 | + return False |
| 169 | + |
| 170 | + try: |
| 171 | + import ctypes |
| 172 | + |
| 173 | + kernel32 = ctypes.windll.kernel32 |
| 174 | + |
| 175 | + # Get stdin handle |
| 176 | + STD_INPUT_HANDLE = -10 |
| 177 | + stdin_handle = kernel32.GetStdHandle(STD_INPUT_HANDLE) |
| 178 | + |
| 179 | + # Get current console mode |
| 180 | + mode = ctypes.c_ulong() |
| 181 | + if not kernel32.GetConsoleMode(stdin_handle, ctypes.byref(mode)): |
| 182 | + return False |
| 183 | + |
| 184 | + # Save original mode for potential restoration |
| 185 | + _original_ctrl_handler = mode.value |
| 186 | + |
| 187 | + # Console mode flags |
| 188 | + ENABLE_PROCESSED_INPUT = 0x0001 # This makes Ctrl+C generate signals |
| 189 | + |
| 190 | + # Remove ENABLE_PROCESSED_INPUT to disable Ctrl+C signal generation |
| 191 | + new_mode = mode.value & ~ENABLE_PROCESSED_INPUT |
| 192 | + |
| 193 | + if kernel32.SetConsoleMode(stdin_handle, new_mode): |
| 194 | + return True |
| 195 | + return False |
| 196 | + |
| 197 | + except Exception: |
| 198 | + return False |
| 199 | + |
| 200 | + |
| 201 | +def enable_windows_ctrl_c() -> bool: |
| 202 | + """Re-enable Ctrl+C at the Windows console level. |
| 203 | +
|
| 204 | + Restores the original console mode saved by disable_windows_ctrl_c(). |
| 205 | +
|
| 206 | + Returns: |
| 207 | + True if successfully re-enabled, False otherwise. |
| 208 | + """ |
| 209 | + global _original_ctrl_handler |
| 210 | + |
| 211 | + if platform.system() != "Windows": |
| 212 | + return False |
| 213 | + |
| 214 | + if _original_ctrl_handler is None: |
| 215 | + return True # Nothing to restore |
| 216 | + |
| 217 | + try: |
| 218 | + import ctypes |
| 219 | + |
| 220 | + kernel32 = ctypes.windll.kernel32 |
| 221 | + |
| 222 | + # Get stdin handle |
| 223 | + STD_INPUT_HANDLE = -10 |
| 224 | + stdin_handle = kernel32.GetStdHandle(STD_INPUT_HANDLE) |
| 225 | + |
| 226 | + # Restore original mode |
| 227 | + if kernel32.SetConsoleMode(stdin_handle, _original_ctrl_handler): |
| 228 | + _original_ctrl_handler = None |
| 229 | + return True |
| 230 | + return False |
| 231 | + |
| 232 | + except Exception: |
| 233 | + return False |
| 234 | + |
| 235 | + |
| 236 | +# Flag to track if we should keep Ctrl+C disabled |
| 237 | +_keep_ctrl_c_disabled: bool = False |
| 238 | + |
| 239 | + |
| 240 | +def set_keep_ctrl_c_disabled(value: bool) -> None: |
| 241 | + """Set whether Ctrl+C should be kept disabled. |
| 242 | +
|
| 243 | + When True, ensure_ctrl_c_disabled() will re-disable Ctrl+C |
| 244 | + even if something else (like prompt_toolkit) re-enables it. |
| 245 | + """ |
| 246 | + global _keep_ctrl_c_disabled |
| 247 | + _keep_ctrl_c_disabled = value |
| 248 | + |
| 249 | + |
| 250 | +def ensure_ctrl_c_disabled() -> bool: |
| 251 | + """Ensure Ctrl+C is disabled if it should be. |
| 252 | +
|
| 253 | + Call this after operations that might restore console mode |
| 254 | + (like prompt_toolkit input). |
| 255 | +
|
| 256 | + Returns: |
| 257 | + True if Ctrl+C is now disabled (or wasn't needed), False on error. |
| 258 | + """ |
| 259 | + if not _keep_ctrl_c_disabled: |
| 260 | + return True |
| 261 | + |
| 262 | + if platform.system() != "Windows": |
| 263 | + return True |
| 264 | + |
| 265 | + try: |
| 266 | + import ctypes |
| 267 | + |
| 268 | + kernel32 = ctypes.windll.kernel32 |
| 269 | + |
| 270 | + # Get stdin handle |
| 271 | + STD_INPUT_HANDLE = -10 |
| 272 | + stdin_handle = kernel32.GetStdHandle(STD_INPUT_HANDLE) |
| 273 | + |
| 274 | + # Get current console mode |
| 275 | + mode = ctypes.c_ulong() |
| 276 | + if not kernel32.GetConsoleMode(stdin_handle, ctypes.byref(mode)): |
| 277 | + return False |
| 278 | + |
| 279 | + # Console mode flags |
| 280 | + ENABLE_PROCESSED_INPUT = 0x0001 |
| 281 | + |
| 282 | + # Check if Ctrl+C processing is enabled |
| 283 | + if mode.value & ENABLE_PROCESSED_INPUT: |
| 284 | + # Disable it |
| 285 | + new_mode = mode.value & ~ENABLE_PROCESSED_INPUT |
| 286 | + return bool(kernel32.SetConsoleMode(stdin_handle, new_mode)) |
| 287 | + |
| 288 | + return True # Already disabled |
| 289 | + |
| 290 | + except Exception: |
| 291 | + return False |
0 commit comments