-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathmain.py
More file actions
678 lines (568 loc) · 29.5 KB
/
main.py
File metadata and controls
678 lines (568 loc) · 29.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
import sys
import signal
import time
import logging
# Conditionally import keyboard or pynput
if sys.platform == 'win32':
try:
import keyboard
except ImportError:
print("Error: keyboard library not found. Please install it using:")
print("pip install keyboard")
sys.exit(1)
elif sys.platform == 'darwin':
try:
from pynput import keyboard as pynput_keyboard
except ImportError:
print("Error: pynput library not found. Please install it using:")
print("pip install pynput")
sys.exit(1)
else:
# Optionally handle other platforms or raise an error
print(f"Warning: Hotkey support not explicitly implemented for platform '{sys.platform}'.")
pynput_keyboard = None # Ensure the variable exists
keyboard = None
from io import BytesIO
from PIL import Image
import threading
from functools import partial
import os
from datetime import datetime
from PySide6.QtWidgets import QApplication, QMessageBox
from PySide6.QtCore import QTimer, QObject, Signal, Slot, Qt
from overlay import OverlayWindow
from api_client import ApiClient # Import our new ApiClient instead of BackendClient
import config # Import the configuration file
# Add necessary imports
import subprocess
import tempfile
if sys.platform == 'win32':
try:
import mss
import mss.tools
except ImportError:
print("Error: mss not installed. Please run: pip install mss")
sys.exit(1)
# Platform specific imports
if sys.platform == 'darwin':
try:
from Cocoa import NSApplication, NSApp, NSApplicationActivationPolicyAccessory
except ImportError:
print("Error: PyObjC not installed. Please run: pip install pyobjc-framework-cocoa")
sys.exit(1)
# --- High DPI Scaling --- (Set BEFORE QApplication import)
# Enable High DPI scaling for better rendering on scaled displays
os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1"
os.environ["QT_SCALE_FACTOR"] = "1.25" # Start with 1, adjust if needed
# ----------------------
# Set up logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(),
logging.FileHandler(os.path.join(os.path.dirname(__file__), 'cognicoder.log'))
]
)
logger = logging.getLogger(__name__)
# Disable third-party loggers
logging.getLogger('PIL').setLevel(logging.WARNING)
logging.getLogger('requests').setLevel(logging.WARNING)
logging.getLogger('urllib3').setLevel(logging.WARNING)
logging.getLogger('websocket').setLevel(logging.WARNING)
# Change to True to test without the backend
MOCK_MODE = config.MOCK_MODE # Use config value
# API configuration
# API_BASE_URL = "http://localhost:5000" # No longer needed, handled in ApiClient
# Constants for screenshot capture
SCREENSHOT_DELAY_MS = config.SCREENSHOT_DELAY_MS # Use config value
MOVEMENT_STEP = config.OVERLAY_MOVEMENT_STEP # Use config value
# DELAY_SECONDS = 0.5 # Delay for screenshot - replaced by SCREENSHOT_DELAY_MS
class SignalHandler(QObject):
def __init__(self, app):
super().__init__()
self.app = app
def handle_signal(self, signum, frame):
logger.info(f"Signal {signum} received, shutting down...")
self.app.quit()
# Global hotkey handler using platform-specific library
class HotkeyHandler(QObject):
capture_signal = Signal()
toggle_signal = Signal()
process_signal = Signal()
process_fast_signal = Signal() # New signal for fast processing
move_left_signal = Signal()
move_right_signal = Signal()
move_up_signal = Signal()
move_down_signal = Signal()
toggle_capture_visibility_signal = Signal()
reset_screenshots_signal = Signal()
follow_up_signal = Signal()
focus_signal = Signal()
def __init__(self):
super().__init__()
# pynput specific attributes, initialize only if needed
self.listener = None
self.listener_thread = None
def start_listener(self):
"""Starts the appropriate hotkey listener based on the OS."""
if sys.platform == 'win32' and keyboard:
logger.debug("Registering hotkeys using 'keyboard' library (Windows)...")
# Register hotkeys using keyboard library and config values
# Use suppress=True to prevent the key press from propagating
keyboard.add_hotkey(config.HOTKEY_CAPTURE, self.on_capture, suppress=True)
keyboard.add_hotkey(config.HOTKEY_PROCESS, self.on_process, suppress=True)
keyboard.add_hotkey(config.HOTKEY_PROCESS_FAST, self.on_process_fast, suppress=True) # Register fast process hotkey
keyboard.add_hotkey(config.HOTKEY_TOGGLE_VISIBILITY, self.on_toggle, suppress=True)
keyboard.add_hotkey(config.HOTKEY_MOVE_LEFT, self.on_move_left, suppress=True)
keyboard.add_hotkey(config.HOTKEY_MOVE_RIGHT, self.on_move_right, suppress=True)
keyboard.add_hotkey(config.HOTKEY_MOVE_UP, self.on_move_up, suppress=True)
keyboard.add_hotkey(config.HOTKEY_MOVE_DOWN, self.on_move_down, suppress=True)
keyboard.add_hotkey(config.HOTKEY_TOGGLE_CAPTURE_VISIBILITY, self.toggle_capture_visibility, suppress=True)
keyboard.add_hotkey(config.HOTKEY_RESET_SCREENSHOTS, self.on_reset_screenshots, suppress=True)
keyboard.add_hotkey(config.HOTKEY_FOLLOW_UP, self.on_follow_up, suppress=True)
keyboard.add_hotkey(config.HOTKEY_FOCUS_OVERLAY, self.on_focus, suppress=True)
logger.info("'keyboard' hotkeys registered.")
# Note: 'keyboard' library doesn't require a separate listener thread typically.
# It hooks into the system event loop.
elif sys.platform == 'darwin' and pynput_keyboard:
logger.debug("Starting pynput hotkey listener (macOS)...")
# Define hotkeys map for pynput using config values
# Note: pynput uses '<modifier>+<key>' format
def format_pynput_hotkey(hotkey_str):
# Basic replacement for common keys, might need expansion
return hotkey_str.replace('enter', '<enter>') \
.replace('left', '<left>') \
.replace('right', '<right>') \
.replace('up', '<up>') \
.replace('down', '<down>')
hotkeys_map = {
format_pynput_hotkey(config.HOTKEY_CAPTURE): self.on_capture,
format_pynput_hotkey(config.HOTKEY_PROCESS): self.on_process,
format_pynput_hotkey(config.HOTKEY_PROCESS_FAST): self.on_process_fast,
format_pynput_hotkey(config.HOTKEY_TOGGLE_VISIBILITY): self.on_toggle,
format_pynput_hotkey(config.HOTKEY_MOVE_LEFT): self.on_move_left,
format_pynput_hotkey(config.HOTKEY_MOVE_RIGHT): self.on_move_right,
format_pynput_hotkey(config.HOTKEY_MOVE_UP): self.on_move_up,
format_pynput_hotkey(config.HOTKEY_MOVE_DOWN): self.on_move_down,
format_pynput_hotkey(config.HOTKEY_TOGGLE_CAPTURE_VISIBILITY): self.toggle_capture_visibility,
format_pynput_hotkey(config.HOTKEY_RESET_SCREENSHOTS): self.on_reset_screenshots,
format_pynput_hotkey(config.HOTKEY_FOLLOW_UP): self.on_follow_up,
format_pynput_hotkey(config.HOTKEY_FOCUS_OVERLAY): self.on_focus
}
# Run listener in a separate thread for pynput
def run_listener():
try:
self.listener = pynput_keyboard.GlobalHotKeys(hotkeys_map)
self.listener.start() # Use start() for non-blocking
logger.info("pynput GlobalHotKeys listener started.")
self.listener.join() # Block this thread until listener stops
logger.info("pynput GlobalHotKeys listener stopped.")
except Exception as e:
logger.error(f"Failed to start or run pynput listener: {e}", exc_info=True)
self.listener_thread = threading.Thread(target=run_listener, daemon=True)
self.listener_thread.start()
else:
logger.warning(f"Hotkey listener not started (unsupported platform '{sys.platform}' or library missing).")
def stop_listener(self):
"""Stops the appropriate hotkey listener based on the OS."""
if sys.platform == 'win32' and keyboard:
logger.info("Unhooking all 'keyboard' hotkeys...")
try:
keyboard.unhook_all()
logger.info("'keyboard' hotkeys unhooked.")
except Exception as e:
logger.error(f"Error unhooking 'keyboard' hotkeys: {e}", exc_info=True)
elif sys.platform == 'darwin' and pynput_keyboard:
if self.listener:
logger.info("Stopping pynput hotkey listener...")
try:
self.listener.stop()
if self.listener_thread and self.listener_thread.is_alive():
self.listener_thread.join(timeout=1.0)
if self.listener_thread.is_alive():
logger.warning("pynput listener thread did not exit cleanly.")
except Exception as e:
logger.error(f"Error stopping pynput listener: {e}", exc_info=True)
self.listener = None
self.listener_thread = None
logger.info("pynput listener stopped.")
else:
logger.debug("pynput listener was not running.")
else:
logger.debug("No active hotkey listener to stop for this platform.")
# --- Signal emitting methods ---
def on_capture(self):
logger.debug("Capture hotkey pressed: <ctrl>+<shift>+h")
self.capture_signal.emit()
def on_toggle(self):
logger.debug("Toggle visibility hotkey pressed: <ctrl>+b")
self.toggle_signal.emit()
def on_process(self):
logger.debug(f"Process hotkey pressed: {config.HOTKEY_PROCESS}")
self.process_signal.emit()
def on_process_fast(self):
logger.debug(f"Process Fast hotkey pressed: {config.HOTKEY_PROCESS_FAST}")
self.process_fast_signal.emit() # Emit the new signal
def on_move_left(self):
logger.debug(f"Move left hotkey pressed: {config.HOTKEY_MOVE_LEFT}")
self.move_left_signal.emit()
def on_move_right(self):
logger.debug(f"Move right hotkey pressed: {config.HOTKEY_MOVE_RIGHT}")
self.move_right_signal.emit()
def on_move_up(self):
logger.debug(f"Move up hotkey pressed: {config.HOTKEY_MOVE_UP}")
self.move_up_signal.emit()
def on_move_down(self):
logger.debug(f"Move down hotkey pressed: {config.HOTKEY_MOVE_DOWN}")
self.move_down_signal.emit()
def toggle_capture_visibility(self):
logger.debug(f"Toggle capture visibility hotkey pressed: {config.HOTKEY_TOGGLE_CAPTURE_VISIBILITY}")
self.toggle_capture_visibility_signal.emit()
def on_reset_screenshots(self):
logger.debug(f"Reset screenshots hotkey pressed: {config.HOTKEY_RESET_SCREENSHOTS}")
self.reset_screenshots_signal.emit()
def on_follow_up(self):
logger.debug(f"Follow-up hotkey pressed: {config.HOTKEY_FOLLOW_UP}")
self.follow_up_signal.emit()
def on_focus(self):
logger.debug(f"Focus overlay hotkey pressed: {config.HOTKEY_FOCUS_OVERLAY}")
self.focus_signal.emit()
# Screenshot and navigation functions
def take_screenshot(overlay):
"""Hides overlay and triggers delayed capture."""
logger.debug("Initiating screenshot capture")
# Hide overlay first
#overlay.hide()
# Schedule the actual screenshot after a delay to ensure overlay is hidden
QTimer.singleShot(config.SCREENSHOT_DELAY_MS, partial(delayed_capture, overlay))
def delayed_capture(overlay):
"""Performs the actual screen capture using platform-specific methods."""
logger.debug("Delayed capture executing")
image_bytes = None
try:
if sys.platform == 'darwin':
# macOS implementation using screencapture utility
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_file:
tmp_filename = tmp_file.name
logger.debug(f"Using temporary file for screenshot: {tmp_filename}")
# Command: screencapture -x (no sound, no cursor) <filename>
command = ["screencapture", "-x", tmp_filename]
try:
# Run the command
result = subprocess.run(command, check=True, capture_output=True, text=True, timeout=10)
logger.debug(f"screencapture command executed successfully.")
# Optional: Check result.stderr for potential warnings, though usually empty on success
if result.stderr:
logger.warning(f"screencapture stderr: {result.stderr.strip()}")
# Read the captured image file using PIL
with Image.open(tmp_filename) as img:
logger.debug(f"Screenshot opened from {tmp_filename} (mode: {img.mode}, size: {img.size})")
# Convert to RGB if it has alpha (PNGs often do)
if img.mode == 'RGBA':
logger.debug("Converting image from RGBA to RGB")
img = img.convert('RGB')
# Save to BytesIO buffer as WEBP
buffer = BytesIO()
img.save(buffer, format="WEBP", quality=70)
image_bytes = buffer.getvalue()
logger.debug(f"Screenshot saved to buffer as WEBP (size: {len(image_bytes)} bytes)")
except FileNotFoundError:
logger.error("Error: 'screencapture' command not found. Ensure macOS is running and the command is in PATH.")
overlay.update_status("Error: screencapture command not found.")
return # Stop processing this screenshot attempt
except subprocess.CalledProcessError as e:
# Log error details from the failed command
logger.error(f"screencapture command failed with return code {e.returncode}")
if e.stdout:
logger.error(f"stdout: {e.stdout.strip()}")
if e.stderr:
logger.error(f"stderr: {e.stderr.strip()}")
overlay.update_status(f"Screenshot failed (screencapture error {e.returncode}).")
return # Stop processing this screenshot attempt
except subprocess.TimeoutExpired:
logger.error("screencapture command timed out after 10 seconds.")
overlay.update_status("Screenshot failed (timeout).")
return # Stop processing this screenshot attempt
except Exception as e:
logger.error(f"Error processing screenshot file {tmp_filename}: {e}", exc_info=True)
overlay.update_status(f"Error reading screenshot: {e}")
return # Stop processing this screenshot attempt
finally:
# Clean up the temporary file regardless of success/failure reading it
if os.path.exists(tmp_filename):
try:
os.remove(tmp_filename)
logger.debug(f"Removed temporary file: {tmp_filename}")
except OSError as e:
logger.warning(f"Could not remove temporary screenshot file {tmp_filename}: {e}")
elif sys.platform == 'win32':
logger.debug("Delayed capture executing")
try:
# Take screenshot
with mss.mss() as sct:
monitor = sct.monitors[1]
screenshot = sct.grab(monitor)
# Convert to PIL Image
img = Image.frombytes("RGB", screenshot.size, screenshot.bgra, "raw", "BGRX")
# Store screenshot
buffer = BytesIO()
img.save(buffer, format="WEBP", quality=70) # Changed from JPEG to WebP with quality 70
image_bytes = buffer.getvalue()
# Store in app global
#QApplication.instance().screenshots.append(image_bytes)
# Update status
overlay.update_status(f"Screenshot {len(QApplication.instance().screenshots)} captured. Press CTRL+SHIFT+ENTER to process.")
# Show overlay again
overlay.show()
except Exception as e:
logger.error(f"Screenshot error: {e}")
overlay.update_status(f"Screenshot error: {e}")
overlay.show()
else:
logger.warning(f"Screenshot functionality not supported on platform: {sys.platform}")
overlay.update_status(f"Screenshot not supported on this OS ({sys.platform}).")
return # Stop processing
# If we got image_bytes, store it
if image_bytes:
app_instance = QApplication.instance()
if not hasattr(app_instance, 'screenshots'):
app_instance.screenshots = [] # Initialize if somehow missing
app_instance.screenshots.append(image_bytes)
screenshot_count = len(app_instance.screenshots)
overlay.update_status(f"Screenshot {screenshot_count} captured. Press CTRL+SHIFT+ENTER to process.")
logger.info(f"Screenshot {screenshot_count} captured and stored.")
else:
# This case should ideally be covered by the returns above, but as a fallback:
logger.warning("Screenshot capture resulted in no image data.")
overlay.update_status("Screenshot capture failed to produce image.")
except Exception as e:
# General error catching for the whole function
logger.error(f"Unexpected error during delayed_capture: {e}", exc_info=True)
overlay.update_status(f"Screenshot error: {e}")
finally:
# Ensure overlay is shown again, even if errors occurred
if not overlay.isVisible():
logger.debug("Showing overlay after capture attempt.")
overlay.show()
def process_screenshots(overlay, fast_mode=False):
"""Initiates screenshot processing via ApiClient.
Args:
overlay: The overlay window instance.
fast_mode: Boolean indicating if fast mode should be used.
"""
logger.debug(f"Processing screenshots (fast_mode={fast_mode})")
screenshots = QApplication.instance().screenshots
if not screenshots:
overlay.update_status(f"No screenshots to process. Press {config.HOTKEY_CAPTURE} to capture.")
return
status_message = "Processing screenshots (Fast Mode)..." if fast_mode else "Processing screenshots..."
overlay.update_status(status_message)
# Reset the output area before starting
overlay.update_output("# Analyzing Problem...\n\n*Processing your screenshots and generating solution...*")
if MOCK_MODE:
# Use mock data for testing without backend
logger.debug("Using mock data (MOCK_MODE is enabled)")
mock_output = """# Two Sum
## Problem Description
Given an array of integers `nums` and an integer `target`, return indices of the two numbers such that they add up to target.
You may assume that each input would have exactly one solution, and you may not use the same element twice.
You can return the answer in any order.
## Constraints
- 2 <= nums.length <= 10^4
- -10^9 <= nums[i] <= 10^9
- -10^9 <= target <= 10^9
- Only one valid answer exists
### Problem Analysis
We need to find two indices in the array whose values sum up to the target.
### Solution Strategy
1. Use a hash map to store values we've seen and their indices
2. For each number, check if (target - current number) exists in the hash map
3. If it does, we found our solution
4. Otherwise, add the current number and its index to the hash map
### Complexity Analysis
- Time Complexity: O(n) where n is the length of nums, as we only traverse the array once
- Space Complexity: O(n) in the worst case if we need to store all numbers in the hash map
### Solution Code (Python)
```python
def twoSum(self, nums: List[int], target: int) -> List[int]:
seen = {} # value -> index
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
return [seen[complement], i]
seen[num] = i
return [] # No solution found
```
### Solution Code (Java)
```java
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (map.containsKey(complement)) {
return new int[] { map.get(complement), i };
}
map.put(nums[i], i);
}
return new int[0]; // No solution found
}
```
### Example Walkthrough
For example, if nums = [2, 7, 11, 15] and target = 9:
1. Start with empty hash map: seen = {}
2. i=0: num=2, complement=7, 7 not in seen, add 2->0 to seen
3. i=1: num=7, complement=2, 2 is in seen with index 0, return [0, 1]
### Additional Test Cases
- nums = [3, 3], target = 6 → Output: [0, 1]
- nums = [3, 2, 4], target = 6 → Output: [1, 2]
- nums = [-1, -2, -3, -4, -5], target = -8 → Output: [2, 4]
### Common Pitfalls
- Be careful with duplicate values: our solution handles this correctly
- Remember that array indices are 0-based
- Make sure to check if the complement exists before adding the current number
"""
# Simulate async process
def mock_response():
time.sleep(1) # Simulate a short delay
overlay.update_output(mock_output)
overlay.update_status("Processing complete. Use Ctrl+Alt+Arrows to move the window.")
QApplication.instance().screenshots = [] # Clear screenshots after processing
# Start mock processing in a separate thread
mock_thread = threading.Thread(target=mock_response)
mock_thread.daemon = True
mock_thread.start()
else:
# Use the direct API client instead of backend
try:
# Use our ApiClient for direct processing
api_client = ApiClient()
# Connect signals
api_client.output_update_signal.connect(overlay.update_output)
api_client.status_update_signal.connect(overlay.update_status)
# *** Add logging here ***
logger.debug(f"[main.py] Calling api_client.process_images with fast_mode={fast_mode}")
# Process the images directly
result = api_client.process_images(screenshots, fast_mode=fast_mode)
# We don't need to store last_problem_data in the app instance anymore
# since we're using static class variables in ApiClient
# Processing continues asynchronously, will update UI via signals
except Exception as e:
logger.error(f"Error in API processing: {e}")
overlay.update_status(f"Error: {str(e)}")
overlay.update_output(f"# Error Processing Screenshots\n\nThere was an error processing your screenshots:\n\n```\n{str(e)}\n```\n\nPlease try again.")
QApplication.instance().screenshots = []
def move_overlay(overlay, direction):
logger.debug(f"Moving overlay: {direction}")
pos = overlay.pos()
step = config.OVERLAY_MOVEMENT_STEP # Use config value
if direction == 'left':
overlay.move(pos.x() - step, pos.y())
elif direction == 'right':
overlay.move(pos.x() + step, pos.y())
elif direction == 'up':
overlay.move(pos.x(), pos.y() - step)
elif direction == 'down':
overlay.move(pos.x(), pos.y() + step)
def reset_screenshots(overlay):
"""Reset/clear all captured screenshots"""
app = QApplication.instance()
previous_count = len(app.screenshots) if hasattr(app, 'screenshots') else 0
# Clear screenshots
app.screenshots = []
# Update UI
overlay.update_status(f"Screenshots reset. {previous_count} screenshot(s) cleared.")
logger.debug(f"Reset {previous_count} screenshots")
def show_follow_up_dialog(overlay):
"""Show the follow-up input at the bottom of the overlay"""
# Check if there is context to follow up on using the static solution content variable
if not ApiClient._last_solution_content:
logger.debug("Follow-up requested, but no previous context found in _last_solution_content.")
overlay.update_status("No previous solution to follow up on. Process a problem first.")
return
# If context exists, proceed to show the input field in the overlay
logger.debug("Follow-up requested, context found in _last_solution_content. Showing input.")
overlay.show_follow_up_input()
def main():
# --- macOS Specific Setup ---
# Moved policy setting to after QApplication init
# if sys.platform == 'darwin':
# # Hide Dock icon by setting activation policy BEFORE QApplication init
# try:
# logger.debug("Setting macOS Activation Policy to Accessory")
# app_instance = NSApplication.sharedApplication()
# app_instance.setActivationPolicy_(NSApplicationActivationPolicyAccessory)
# logger.debug("macOS Activation Policy set.")
# except Exception as e:
# logger.error(f"Failed to set macOS Activation Policy: {e}")
# ---------------------------
# Create Qt application
app = QApplication(sys.argv)
# --- macOS Specific Setup (Attempt 2: After QApplication) ---
if sys.platform == 'darwin' and 'NSApplication' in globals(): # Check if import succeeded
try:
logger.debug("Attempting to set macOS Activation Policy to Accessory (after QApplication init)")
# NSApp should be available now
NSApp.setActivationPolicy_(NSApplicationActivationPolicyAccessory)
logger.info("macOS Activation Policy set to Accessory.")
except Exception as e:
logger.error(f"Failed to set macOS Activation Policy (after QApplication init): {e}", exc_info=True)
# ---------------------------------------------------------
# Set up signal handling
signal_handler = SignalHandler(app)
# Set up signal handlers for SIGINT (Ctrl+C) and SIGTERM
signal.signal(signal.SIGINT, signal_handler.handle_signal)
signal.signal(signal.SIGTERM, signal_handler.handle_signal)
# Install a signal handler for Qt's event loop
# This ensures signal delivery even during Qt event processing
timer = QTimer()
timer.start(500) # Check for signals every 500ms
timer.timeout.connect(lambda: None) # Just wake up the event loop
# Create overlay window
overlay = OverlayWindow()
# Create screenshots list
app.screenshots = []
# Create and start hotkey handler
hotkey_handler = HotkeyHandler()
hotkey_handler.start_listener() # Start the listener thread
# Connect signals to slots
hotkey_handler.capture_signal.connect(lambda: take_screenshot(overlay))
hotkey_handler.toggle_signal.connect(overlay.toggle_visibility)
hotkey_handler.process_signal.connect(lambda: process_screenshots(overlay, fast_mode=False))
hotkey_handler.process_fast_signal.connect(lambda: process_screenshots(overlay, fast_mode=True)) # Connect fast signal
hotkey_handler.move_left_signal.connect(lambda: move_overlay(overlay, 'left'))
hotkey_handler.move_right_signal.connect(lambda: move_overlay(overlay, 'right'))
hotkey_handler.move_up_signal.connect(lambda: move_overlay(overlay, 'up'))
hotkey_handler.move_down_signal.connect(lambda: move_overlay(overlay, 'down'))
hotkey_handler.toggle_capture_visibility_signal.connect(overlay.toggle_capture_visibility)
hotkey_handler.reset_screenshots_signal.connect(lambda: reset_screenshots(overlay))
hotkey_handler.follow_up_signal.connect(lambda: show_follow_up_dialog(overlay))
hotkey_handler.focus_signal.connect(overlay.bring_to_front)
# Initialize UI
overlay.update_status(f"Ready. Press {config.HOTKEY_CAPTURE} to capture screen.")
# Welcome message with instructions
welcome_msg = f"""# Welcome to Acecoder
## Quick Instructions
1. Press **{config.HOTKEY_CAPTURE}** to capture a screenshot of the coding problem
2. Press **{config.HOTKEY_PROCESS}** to process the captured screenshots (standard analysis)
3. Press **{config.HOTKEY_PROCESS_FAST}** to process screenshots quickly (skips content detection)
4. Use **{config.HOTKEY_MOVE_LEFT} / {config.HOTKEY_MOVE_RIGHT} / {config.HOTKEY_MOVE_UP} / {config.HOTKEY_MOVE_DOWN}** to move this window around
5. Press **{config.HOTKEY_TOGGLE_VISIBILITY}** to hide/show this overlay
6. Press **{config.HOTKEY_FOLLOW_UP}** to open the follow-up chat when you need help with the solution
7. Press **{config.HOTKEY_RESET_SCREENSHOTS}** to clear captured screenshots
8. Press **{config.HOTKEY_TOGGLE_CAPTURE_VISIBILITY}** to toggle window visibility in captures
The assistant will analyze the problem and provide a solution. If you have questions or encounter errors with the solution, use the follow-up feature to get additional help without losing context."""
overlay.update_output(welcome_msg)
# Create a cleanup handler
def cleanup():
logger.info("Cleaning up before exit...")
hotkey_handler.stop_listener() # Stop the appropriate listener
app.aboutToQuit.connect(cleanup)
return app.exec()
if __name__ == "__main__":
try:
# Platform-specific library check is now done at the top-level import
# No need for checks here anymore
sys.exit(main())
except KeyboardInterrupt:
logger.info("Application terminated by keyboard interrupt")