code-puppy 0.0.332__py3-none-any.whl → 0.0.334__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,426 @@
1
+ """Interactive TUI onboarding wizard for first-time Code Puppy users.
2
+
3
+ 🐶 Welcome to Code Puppy! This wizard guides new users through initial setup,
4
+ model configuration, and feature discovery. Uses the same TUI patterns as
5
+ colors_menu.py and diff_menu.py for a consistent experience.
6
+
7
+ Usage:
8
+ from code_puppy.command_line.onboarding_wizard import (
9
+ should_show_onboarding,
10
+ run_onboarding_wizard,
11
+ )
12
+
13
+ if should_show_onboarding():
14
+ result = await run_onboarding_wizard()
15
+ # result: "chatgpt", "claude", "completed", "skipped", or None
16
+ """
17
+
18
+ import io
19
+ import os
20
+ import sys
21
+ import time
22
+ from typing import List, Optional, Tuple
23
+
24
+ from prompt_toolkit import Application
25
+ from prompt_toolkit.formatted_text import ANSI, FormattedText
26
+ from prompt_toolkit.key_binding import KeyBindings
27
+ from prompt_toolkit.layout import Layout, Window
28
+ from prompt_toolkit.layout.controls import FormattedTextControl
29
+ from prompt_toolkit.widgets import Frame
30
+ from rich.console import Console
31
+
32
+ from code_puppy.config import CONFIG_DIR
33
+
34
+ from .onboarding_slides import (
35
+ MODEL_OPTIONS,
36
+ slide_agent_system,
37
+ slide_api_keys,
38
+ slide_appearance,
39
+ slide_complete,
40
+ slide_mcp_servers,
41
+ slide_model_pinning,
42
+ slide_model_settings,
43
+ slide_model_subscription,
44
+ slide_planning_agent,
45
+ slide_welcome,
46
+ )
47
+
48
+ # ============================================================================
49
+ # State Tracking (like motd.py pattern)
50
+ # ============================================================================
51
+
52
+ ONBOARDING_COMPLETE_FILE = os.path.join(CONFIG_DIR, "onboarding_complete")
53
+
54
+
55
+ def has_completed_onboarding() -> bool:
56
+ """Check if the user has already completed onboarding.
57
+
58
+ Returns:
59
+ True if onboarding has been completed, False otherwise.
60
+ """
61
+ return os.path.exists(ONBOARDING_COMPLETE_FILE)
62
+
63
+
64
+ def mark_onboarding_complete() -> None:
65
+ """Mark onboarding as complete by creating the tracking file."""
66
+ os.makedirs(os.path.dirname(ONBOARDING_COMPLETE_FILE), exist_ok=True)
67
+ with open(ONBOARDING_COMPLETE_FILE, "w") as f:
68
+ f.write("completed\n")
69
+
70
+
71
+ def should_show_onboarding() -> bool:
72
+ """Determine if the onboarding wizard should be shown.
73
+
74
+ Returns:
75
+ True if onboarding should be shown, False otherwise.
76
+ """
77
+ return not has_completed_onboarding()
78
+
79
+
80
+ def reset_onboarding() -> None:
81
+ """Reset onboarding state (useful for testing or re-running wizard)."""
82
+ if os.path.exists(ONBOARDING_COMPLETE_FILE):
83
+ os.remove(ONBOARDING_COMPLETE_FILE)
84
+
85
+
86
+ # ============================================================================
87
+ # Onboarding Wizard Class
88
+ # ============================================================================
89
+
90
+
91
+ class OnboardingWizard:
92
+ """Interactive onboarding wizard with slide-based navigation.
93
+
94
+ Attributes:
95
+ current_slide: Index of the currently displayed slide (0-9)
96
+ selected_option: Index of selected option within current slide
97
+ trigger_oauth: OAuth provider to trigger after wizard ("chatgpt"/"claude")
98
+ model_choice: User's model subscription selection
99
+ """
100
+
101
+ TOTAL_SLIDES = 10
102
+
103
+ def __init__(self):
104
+ """Initialize the onboarding wizard state."""
105
+ self.current_slide = 0
106
+ self.selected_option = 0
107
+ self.trigger_oauth: Optional[str] = None
108
+ self.model_choice: Optional[str] = None
109
+ self.result: Optional[str] = None
110
+ self._should_exit = False
111
+
112
+ def get_progress_indicator(self) -> str:
113
+ """Generate progress dots showing current slide position.
114
+
115
+ Returns:
116
+ String like "● ○ ○ ○ ○ ○ ○ ○ ○ ○" for slide 0.
117
+ """
118
+ dots = []
119
+ for i in range(self.TOTAL_SLIDES):
120
+ if i == self.current_slide:
121
+ dots.append("●")
122
+ else:
123
+ dots.append("○")
124
+ return " ".join(dots)
125
+
126
+ def get_slide_content(self) -> str:
127
+ """Get combined content for the current slide.
128
+
129
+ Returns:
130
+ Rich markup string for the slide content.
131
+ """
132
+ if self.current_slide == 0:
133
+ return slide_welcome()
134
+ elif self.current_slide == 1:
135
+ options = self.get_options_for_slide()
136
+ return slide_model_subscription(self.selected_option, options)
137
+ elif self.current_slide == 2:
138
+ return slide_api_keys(self.model_choice)
139
+ elif self.current_slide == 3:
140
+ return slide_mcp_servers()
141
+ elif self.current_slide == 4:
142
+ return slide_appearance()
143
+ elif self.current_slide == 5:
144
+ return slide_agent_system()
145
+ elif self.current_slide == 6:
146
+ return slide_model_pinning()
147
+ elif self.current_slide == 7:
148
+ return slide_planning_agent()
149
+ elif self.current_slide == 8:
150
+ return slide_model_settings()
151
+ else: # slide 9
152
+ return slide_complete(self.trigger_oauth)
153
+
154
+ def get_options_for_slide(self) -> List[Tuple[str, str]]:
155
+ """Get selectable options for the current slide.
156
+
157
+ Returns:
158
+ List of (id, label) tuples for options, or empty list if no options.
159
+ """
160
+ if self.current_slide == 1: # Model subscription slide
161
+ return [(opt[0], opt[1]) for opt in MODEL_OPTIONS]
162
+ return []
163
+
164
+ def handle_option_select(self) -> None:
165
+ """Handle selection of the current option."""
166
+ if self.current_slide == 1: # Model subscription
167
+ options = self.get_options_for_slide()
168
+ if 0 <= self.selected_option < len(options):
169
+ choice_id = options[self.selected_option][0]
170
+ self.model_choice = choice_id
171
+
172
+ # Set OAuth trigger for ChatGPT/Claude
173
+ if choice_id == "chatgpt":
174
+ self.trigger_oauth = "chatgpt"
175
+ elif choice_id == "claude":
176
+ self.trigger_oauth = "claude"
177
+
178
+ def next_slide(self) -> bool:
179
+ """Move to the next slide.
180
+
181
+ Returns:
182
+ True if moved to next slide, False if at last slide.
183
+ """
184
+ if self.current_slide < self.TOTAL_SLIDES - 1:
185
+ self.current_slide += 1
186
+ self.selected_option = 0
187
+ return True
188
+ return False
189
+
190
+ def prev_slide(self) -> bool:
191
+ """Move to the previous slide.
192
+
193
+ Returns:
194
+ True if moved to previous slide, False if at first slide.
195
+ """
196
+ if self.current_slide > 0:
197
+ self.current_slide -= 1
198
+ self.selected_option = 0
199
+ return True
200
+ return False
201
+
202
+ def next_option(self) -> None:
203
+ """Move to the next option within the current slide."""
204
+ options = self.get_options_for_slide()
205
+ if options:
206
+ self.selected_option = (self.selected_option + 1) % len(options)
207
+
208
+ def prev_option(self) -> None:
209
+ """Move to the previous option within the current slide."""
210
+ options = self.get_options_for_slide()
211
+ if options:
212
+ self.selected_option = (self.selected_option - 1) % len(options)
213
+
214
+
215
+ # ============================================================================
216
+ # TUI Rendering Functions
217
+ # ============================================================================
218
+
219
+
220
+ def _get_slide_panel_content(wizard: OnboardingWizard) -> ANSI:
221
+ """Generate the centered slide content.
222
+
223
+ Args:
224
+ wizard: The OnboardingWizard instance.
225
+
226
+ Returns:
227
+ ANSI object with formatted slide content.
228
+ """
229
+ buffer = io.StringIO()
230
+ console = Console(
231
+ file=buffer,
232
+ force_terminal=True,
233
+ width=80,
234
+ legacy_windows=False,
235
+ color_system="truecolor",
236
+ no_color=False,
237
+ force_interactive=True,
238
+ )
239
+
240
+ # Progress indicator
241
+ progress = wizard.get_progress_indicator()
242
+ console.print(f"[dim]{progress}[/dim]\n")
243
+
244
+ # Slide number
245
+ console.print(
246
+ f"[dim]Slide {wizard.current_slide + 1} of {wizard.TOTAL_SLIDES}[/dim]\n\n"
247
+ )
248
+
249
+ # Combined slide content
250
+ slide_content = wizard.get_slide_content()
251
+ console.print(slide_content)
252
+
253
+ return ANSI(buffer.getvalue())
254
+
255
+
256
+ def _get_navigation_hints(wizard: OnboardingWizard) -> FormattedText:
257
+ """Generate navigation hints for the bottom of the screen.
258
+
259
+ Args:
260
+ wizard: The OnboardingWizard instance.
261
+
262
+ Returns:
263
+ FormattedText with navigation hints.
264
+ """
265
+ hints = []
266
+
267
+ if wizard.current_slide > 0:
268
+ hints.append(("fg:ansicyan", "← Back "))
269
+
270
+ if wizard.current_slide < wizard.TOTAL_SLIDES - 1:
271
+ hints.append(("fg:ansicyan", "→ Next "))
272
+ else:
273
+ hints.append(("fg:ansigreen bold", "Enter: Finish "))
274
+
275
+ options = wizard.get_options_for_slide()
276
+ if options:
277
+ hints.append(("fg:ansicyan", "↑↓ Options "))
278
+
279
+ hints.append(("fg:ansiyellow", "ESC: Skip"))
280
+
281
+ return FormattedText(hints)
282
+
283
+
284
+ # ============================================================================
285
+ # Main Entry Point
286
+ # ============================================================================
287
+
288
+
289
+ async def run_onboarding_wizard() -> Optional[str]:
290
+ """Run the interactive onboarding wizard.
291
+
292
+ Returns:
293
+ - "chatgpt" if user wants ChatGPT OAuth
294
+ - "claude" if user wants Claude OAuth
295
+ - "completed" if finished normally
296
+ - "skipped" if user pressed ESC
297
+ - None on error
298
+ """
299
+ from code_puppy.tools.command_runner import set_awaiting_user_input
300
+
301
+ wizard = OnboardingWizard()
302
+
303
+ set_awaiting_user_input(True)
304
+
305
+ # Enter alternate screen buffer
306
+ sys.stdout.write("\033[?1049h") # Enter alternate buffer
307
+ sys.stdout.write("\033[2J\033[H") # Clear and home
308
+ sys.stdout.flush()
309
+ time.sleep(0.1) # Minimal delay for state sync
310
+
311
+ try:
312
+ # Set up key bindings
313
+ kb = KeyBindings()
314
+
315
+ @kb.add("right")
316
+ @kb.add("l")
317
+ def next_slide(event):
318
+ if wizard.current_slide == wizard.TOTAL_SLIDES - 1:
319
+ # On last slide, right arrow finishes
320
+ wizard.result = "completed"
321
+ wizard._should_exit = True
322
+ event.app.exit()
323
+ else:
324
+ wizard.next_slide()
325
+ event.app.invalidate()
326
+
327
+ @kb.add("left")
328
+ @kb.add("h")
329
+ def prev_slide(event):
330
+ wizard.prev_slide()
331
+ event.app.invalidate()
332
+
333
+ @kb.add("down")
334
+ @kb.add("j")
335
+ def next_option(event):
336
+ wizard.next_option()
337
+ event.app.invalidate()
338
+
339
+ @kb.add("up")
340
+ @kb.add("k")
341
+ def prev_option(event):
342
+ wizard.prev_option()
343
+ event.app.invalidate()
344
+
345
+ @kb.add("enter")
346
+ def select_or_next(event):
347
+ # Handle option selection on slides with options
348
+ options = wizard.get_options_for_slide()
349
+ if options:
350
+ wizard.handle_option_select()
351
+
352
+ # Move to next slide or finish
353
+ if wizard.current_slide == wizard.TOTAL_SLIDES - 1:
354
+ wizard.result = "completed"
355
+ wizard._should_exit = True
356
+ event.app.exit()
357
+ else:
358
+ wizard.next_slide()
359
+ event.app.invalidate()
360
+
361
+ @kb.add("escape")
362
+ def skip_wizard(event):
363
+ wizard.result = "skipped"
364
+ wizard._should_exit = True
365
+ event.app.exit()
366
+
367
+ @kb.add("c-c")
368
+ def cancel_wizard(event):
369
+ wizard.result = "skipped"
370
+ wizard._should_exit = True
371
+ event.app.exit()
372
+
373
+ # Create layout with single centered panel
374
+ slide_panel = Window(
375
+ content=FormattedTextControl(lambda: _get_slide_panel_content(wizard))
376
+ )
377
+
378
+ root_container = Frame(slide_panel, title="🐶 Welcome to Code Puppy!")
379
+
380
+ layout = Layout(root_container)
381
+
382
+ app = Application(
383
+ layout=layout,
384
+ key_bindings=kb,
385
+ full_screen=False,
386
+ mouse_support=False,
387
+ color_depth="DEPTH_24_BIT",
388
+ )
389
+
390
+ # Clear screen before running
391
+ sys.stdout.write("\033[2J\033[H")
392
+ sys.stdout.flush()
393
+
394
+ # Run the application
395
+ await app.run_async()
396
+
397
+ except KeyboardInterrupt:
398
+ wizard.result = "skipped"
399
+ except Exception:
400
+ wizard.result = None
401
+ finally:
402
+ set_awaiting_user_input(False)
403
+ # Exit alternate screen buffer
404
+ sys.stdout.write("\033[?1049l")
405
+ sys.stdout.flush()
406
+
407
+ # Mark onboarding as complete (even if skipped - they saw it)
408
+ if wizard.result in ("completed", "skipped"):
409
+ mark_onboarding_complete()
410
+
411
+ # Return OAuth trigger if selected, otherwise the result
412
+ if wizard.trigger_oauth:
413
+ return wizard.trigger_oauth
414
+
415
+ return wizard.result
416
+
417
+
418
+ async def run_onboarding_if_needed() -> Optional[str]:
419
+ """Run onboarding wizard if user hasn't completed it yet.
420
+
421
+ Returns:
422
+ Result from run_onboarding_wizard() or None if not needed.
423
+ """
424
+ if should_show_onboarding():
425
+ return await run_onboarding_wizard()
426
+ return None
code_puppy/config.py CHANGED
@@ -1051,11 +1051,11 @@ def set_enable_dbos(enabled: bool) -> None:
1051
1051
  set_config_value("enable_dbos", "true" if enabled else "false")
1052
1052
 
1053
1053
 
1054
- def get_message_limit(default: int = 100) -> int:
1054
+ def get_message_limit(default: int = 1000) -> int:
1055
1055
  """
1056
1056
  Returns the user-configured message/request limit for the agent.
1057
1057
  This controls how many steps/requests the agent can take.
1058
- Defaults to 100 if unset or misconfigured.
1058
+ Defaults to 1000 if unset or misconfigured.
1059
1059
  Configurable by 'message_limit' key.
1060
1060
  """
1061
1061
  val = get_value("message_limit")
code_puppy/keymap.py CHANGED
@@ -55,11 +55,19 @@ class KeymapError(Exception):
55
55
  def get_cancel_agent_key() -> str:
56
56
  """Get the configured cancel agent key from config.
57
57
 
58
+ On Windows when launched via uvx, this automatically returns "ctrl+k"
59
+ to work around uvx capturing Ctrl+C before it reaches Python.
60
+
58
61
  Returns:
59
62
  The key name (e.g., "ctrl+c", "ctrl+k") from config,
60
63
  or the default if not configured.
61
64
  """
62
65
  from code_puppy.config import get_value
66
+ from code_puppy.uvx_detection import should_use_alternate_cancel_key
67
+
68
+ # On Windows + uvx, force ctrl+k to bypass uvx's SIGINT capture
69
+ if should_use_alternate_cancel_key():
70
+ return "ctrl+k"
63
71
 
64
72
  key = get_value("cancel_agent_key")
65
73
  if key is None or key.strip() == "":
@@ -6,6 +6,7 @@ import os
6
6
  from typing import List, Optional, Tuple
7
7
 
8
8
  from code_puppy.callbacks import register_callback
9
+ from code_puppy.config import set_model_name
9
10
  from code_puppy.messaging import emit_info, emit_success, emit_warning
10
11
 
11
12
  from .config import CHATGPT_OAUTH_CONFIG, get_token_storage_path
@@ -75,6 +76,7 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]:
75
76
 
76
77
  if name == "chatgpt-auth":
77
78
  run_oauth_flow()
79
+ set_model_name("chatgpt-gpt-5.2-codex")
78
80
  return True
79
81
 
80
82
  if name == "chatgpt-status":
@@ -12,6 +12,7 @@ from typing import Any, Dict, List, Optional, Tuple
12
12
  from urllib.parse import parse_qs, urlparse
13
13
 
14
14
  from code_puppy.callbacks import register_callback
15
+ from code_puppy.config import set_model_name
15
16
  from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
16
17
 
17
18
  from ..oauth_puppy_html import oauth_failure_html, oauth_success_html
@@ -260,6 +261,7 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]:
260
261
  "Existing Claude Code tokens found. Continuing will overwrite them."
261
262
  )
262
263
  _perform_authentication()
264
+ set_model_name("claude-code-claude-opus-4-5-20251101")
263
265
  return True
264
266
 
265
267
  if name == "claude-code-status":
@@ -6,6 +6,10 @@ Handles Windows console mode resets and Unix terminal sanity restoration.
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,21 +90,36 @@ def reset_windows_console_mode() -> None:
86
90
  pass # Silently ignore errors - best effort reset
87
91
 
88
92
 
89
- def reset_windows_terminal_full() -> None:
90
- """Perform a full Windows terminal reset (console mode + ANSI).
93
+ def flush_windows_keyboard_buffer() -> None:
94
+ """Flush the Windows keyboard buffer.
91
95
 
92
- Combines both console mode reset and ANSI reset for complete
93
- terminal state restoration after interrupts.
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
94
101
 
95
- IMPORTANT: Console mode must be reset FIRST to re-enable
96
- ENABLE_VIRTUAL_TERMINAL_PROCESSING, otherwise the ANSI escape
97
- sequences will be printed as literal text (e.g., '[0m').
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
+
111
+ def reset_windows_terminal_full() -> None:
112
+ """Perform a full Windows terminal reset (ANSI + console mode + keyboard buffer).
113
+
114
+ Combines ANSI reset, console mode reset, and keyboard buffer flush
115
+ for complete terminal state restoration after interrupts.
98
116
  """
99
117
  if platform.system() != "Windows":
100
118
  return
101
119
 
102
- reset_windows_console_mode() # Must be first! Enables ANSI processing
103
- reset_windows_terminal_ansi() # Now ANSI escapes will be interpreted
120
+ reset_windows_terminal_ansi()
121
+ reset_windows_console_mode()
122
+ flush_windows_keyboard_buffer()
104
123
 
105
124
 
106
125
  def reset_unix_terminal() -> None:
@@ -128,3 +147,145 @@ def reset_terminal() -> None:
128
147
  reset_windows_terminal_full()
129
148
  else:
130
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