create-screenfix 0.1.0

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.
Files changed (34) hide show
  1. package/bin/create-screenfix.js +40 -0
  2. package/lib/doctor.js +120 -0
  3. package/lib/init.js +165 -0
  4. package/lib/uninstall.js +67 -0
  5. package/lib/update.js +62 -0
  6. package/package.json +35 -0
  7. package/python/build/lib/screenfix/__init__.py +3 -0
  8. package/python/build/lib/screenfix/annotation_window.py +222 -0
  9. package/python/build/lib/screenfix/clipboard_watcher.py +164 -0
  10. package/python/build/lib/screenfix/config.py +66 -0
  11. package/python/build/lib/screenfix/daemon.py +204 -0
  12. package/python/build/lib/screenfix/mcp_server.py +318 -0
  13. package/python/build/lib/screenfix/task_tracker.py +129 -0
  14. package/python/pyproject.toml +24 -0
  15. package/python/requirements.txt +5 -0
  16. package/python/screenfix/__init__.py +3 -0
  17. package/python/screenfix/annotation_window.py +222 -0
  18. package/python/screenfix/clipboard_watcher.py +164 -0
  19. package/python/screenfix/config.py +66 -0
  20. package/python/screenfix/daemon.py +204 -0
  21. package/python/screenfix/mcp_server.py +318 -0
  22. package/python/screenfix/task_tracker.py +129 -0
  23. package/python/screenfix.egg-info/PKG-INFO +11 -0
  24. package/python/screenfix.egg-info/SOURCES.txt +14 -0
  25. package/python/screenfix.egg-info/dependency_links.txt +1 -0
  26. package/python/screenfix.egg-info/entry_points.txt +3 -0
  27. package/python/screenfix.egg-info/requires.txt +5 -0
  28. package/python/screenfix.egg-info/top_level.txt +1 -0
  29. package/templates/commands/screenfix-list-tasks.md +6 -0
  30. package/templates/commands/screenfix-start.md +8 -0
  31. package/templates/commands/screenfix-status.md +6 -0
  32. package/templates/commands/screenfix-stop.md +6 -0
  33. package/templates/commands/screenfix-tasks-next.md +14 -0
  34. package/templates/commands/screenfix-tasks-yolo.md +14 -0
@@ -0,0 +1,222 @@
1
+ """Annotation window for adding instructions to screenshots."""
2
+
3
+ import os
4
+ import shutil
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+
8
+ from AppKit import (
9
+ NSWindow,
10
+ NSWindowStyleMaskTitled,
11
+ NSWindowStyleMaskClosable,
12
+ NSWindowStyleMaskNonactivatingPanel,
13
+ NSBackingStoreBuffered,
14
+ NSImageView,
15
+ NSImage,
16
+ NSScrollView,
17
+ NSTextView,
18
+ NSButton,
19
+ NSBezelStyleRounded,
20
+ NSTextField,
21
+ NSFont,
22
+ NSApplication,
23
+ NSImageScaleProportionallyDown,
24
+ NSPanel,
25
+ NSWindowCollectionBehaviorCanJoinAllSpaces,
26
+ NSWindowCollectionBehaviorFullScreenAuxiliary,
27
+ )
28
+ from Quartz import kCGMaximumWindowLevelKey, CGWindowLevelForKey
29
+ from Foundation import NSMakeRect, NSObject
30
+ import objc
31
+
32
+ from .config import config
33
+ from .task_tracker import add_task
34
+
35
+
36
+ _current_controller = None
37
+
38
+
39
+ def save_screenshot(temp_path: str) -> str:
40
+ """Save screenshot from temp location to final location."""
41
+ save_dir = Path(config.save_directory)
42
+ save_dir.mkdir(parents=True, exist_ok=True)
43
+
44
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
45
+ filename = f"screenshot_{timestamp}.png"
46
+ final_path = save_dir / filename
47
+
48
+ shutil.move(temp_path, final_path)
49
+ return str(final_path)
50
+
51
+
52
+ def cleanup_temp_file(temp_path: str):
53
+ """Remove temporary file."""
54
+ try:
55
+ if os.path.exists(temp_path):
56
+ os.remove(temp_path)
57
+ except OSError:
58
+ pass
59
+
60
+
61
+ class AnnotationWindowDelegate(NSObject):
62
+ """Delegate to handle window events."""
63
+
64
+ def initWithController_(self, controller):
65
+ self = objc.super(AnnotationWindowDelegate, self).init()
66
+ if self is None:
67
+ return None
68
+ self.controller = controller
69
+ return self
70
+
71
+ def windowWillClose_(self, notification):
72
+ """Called when window is about to close."""
73
+ global _current_controller
74
+ _current_controller = None
75
+
76
+
77
+ class AnnotationWindowController(NSObject):
78
+ """Controller for the annotation window."""
79
+
80
+ def initWithImagePath_(self, image_path):
81
+ self = objc.super(AnnotationWindowController, self).init()
82
+ if self is None:
83
+ return None
84
+
85
+ self.image_path = image_path
86
+ self.window = None
87
+ self.text_view = None
88
+ self.delegate = None
89
+
90
+ self._create_window()
91
+ return self
92
+
93
+ def _create_window(self):
94
+ """Create the annotation window."""
95
+ image = NSImage.alloc().initWithContentsOfFile_(self.image_path)
96
+ if not image:
97
+ return
98
+
99
+ img_size = image.size()
100
+
101
+ max_display_width = 400
102
+ max_display_height = 300
103
+
104
+ width_scale = min(1.0, max_display_width / img_size.width) if img_size.width > 0 else 1.0
105
+ height_scale = min(1.0, max_display_height / img_size.height) if img_size.height > 0 else 1.0
106
+ scale = min(width_scale, height_scale)
107
+
108
+ display_width = int(img_size.width * scale)
109
+ display_height = int(img_size.height * scale)
110
+
111
+ padding = 20
112
+ window_width = max(display_width + padding * 2, 450)
113
+ text_area_height = 80
114
+ button_area_height = 50
115
+ label_height = 25
116
+ window_height = display_height + text_area_height + button_area_height + label_height + padding * 3
117
+
118
+ style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskNonactivatingPanel
119
+ self.window = NSPanel.alloc().initWithContentRect_styleMask_backing_defer_(
120
+ NSMakeRect(200, 200, window_width, window_height),
121
+ style,
122
+ NSBackingStoreBuffered,
123
+ False,
124
+ )
125
+ self.window.setTitle_("ScreenFix - Add Instructions")
126
+
127
+ self.window.setFloatingPanel_(True)
128
+ self.window.setHidesOnDeactivate_(False)
129
+
130
+ max_level = CGWindowLevelForKey(kCGMaximumWindowLevelKey)
131
+ self.window.setLevel_(max_level - 1)
132
+
133
+ self.window.setCollectionBehavior_(
134
+ NSWindowCollectionBehaviorCanJoinAllSpaces |
135
+ NSWindowCollectionBehaviorFullScreenAuxiliary
136
+ )
137
+
138
+ self.delegate = AnnotationWindowDelegate.alloc().initWithController_(self)
139
+ self.window.setDelegate_(self.delegate)
140
+
141
+ content = self.window.contentView()
142
+ y_pos = padding
143
+
144
+ # Cancel button
145
+ cancel_btn = NSButton.alloc().initWithFrame_(NSMakeRect(padding, y_pos, 100, 32))
146
+ cancel_btn.setTitle_("Cancel")
147
+ cancel_btn.setBezelStyle_(NSBezelStyleRounded)
148
+ cancel_btn.setTarget_(self)
149
+ cancel_btn.setAction_(objc.selector(self.cancel_, signature=b"v@:@"))
150
+ content.addSubview_(cancel_btn)
151
+
152
+ # Save button
153
+ save_btn = NSButton.alloc().initWithFrame_(NSMakeRect(window_width - padding - 100, y_pos, 100, 32))
154
+ save_btn.setTitle_("Save")
155
+ save_btn.setBezelStyle_(NSBezelStyleRounded)
156
+ save_btn.setTarget_(self)
157
+ save_btn.setAction_(objc.selector(self.save_, signature=b"v@:@"))
158
+ save_btn.setKeyEquivalent_("\r")
159
+ content.addSubview_(save_btn)
160
+
161
+ y_pos += button_area_height
162
+
163
+ # Text view
164
+ scroll_frame = NSMakeRect(padding, y_pos, window_width - padding * 2, text_area_height)
165
+ scroll_view = NSScrollView.alloc().initWithFrame_(scroll_frame)
166
+ scroll_view.setHasVerticalScroller_(True)
167
+ scroll_view.setBorderType_(1)
168
+
169
+ text_frame = NSMakeRect(0, 0, scroll_frame.size.width - 4, scroll_frame.size.height)
170
+ self.text_view = NSTextView.alloc().initWithFrame_(text_frame)
171
+ self.text_view.setFont_(NSFont.systemFontOfSize_(13))
172
+ scroll_view.setDocumentView_(self.text_view)
173
+ content.addSubview_(scroll_view)
174
+
175
+ y_pos += text_area_height + 5
176
+
177
+ # Label
178
+ label = NSTextField.labelWithString_("Instructions for Claude Code:")
179
+ label.setFrame_(NSMakeRect(padding, y_pos, 300, label_height))
180
+ content.addSubview_(label)
181
+
182
+ y_pos += label_height + 5
183
+
184
+ # Image view
185
+ image_view = NSImageView.alloc().initWithFrame_(NSMakeRect(padding, y_pos, display_width, display_height))
186
+ image_view.setImage_(image)
187
+ image_view.setImageScaling_(NSImageScaleProportionallyDown)
188
+ content.addSubview_(image_view)
189
+
190
+ def show(self):
191
+ """Display the window on top of fullscreen apps."""
192
+ self.window.center()
193
+ self.window.orderFrontRegardless()
194
+ self.window.makeKeyWindow()
195
+ self.window.makeFirstResponder_(self.text_view)
196
+
197
+ def cancel_(self, sender):
198
+ """Handle cancel button click."""
199
+ cleanup_temp_file(self.image_path)
200
+ self.window.close()
201
+
202
+ def save_(self, sender):
203
+ """Handle save button click."""
204
+ instructions = self.text_view.string()
205
+ saved_path = save_screenshot(self.image_path)
206
+
207
+ if instructions and instructions.strip():
208
+ add_task(instructions.strip(), saved_path)
209
+
210
+ self.window.close()
211
+
212
+
213
+ def show_annotation_window(image_path: str) -> None:
214
+ """Display the annotation window for a screenshot."""
215
+ global _current_controller
216
+
217
+ if _current_controller and _current_controller.window:
218
+ _current_controller.window.close()
219
+
220
+ _current_controller = AnnotationWindowController.alloc().initWithImagePath_(image_path)
221
+ if _current_controller:
222
+ _current_controller.show()
@@ -0,0 +1,164 @@
1
+ """
2
+ Clipboard watcher for detecting new screenshots.
3
+
4
+ Watches the system clipboard for new image data and triggers
5
+ a callback when detected. Use Cmd+Ctrl+Shift+4 to copy screenshot
6
+ to clipboard (instant, no preview delay).
7
+ """
8
+
9
+ import os
10
+ import time
11
+ import threading
12
+ from pathlib import Path
13
+ from typing import Callable, Optional
14
+
15
+ from AppKit import NSPasteboard, NSPasteboardTypePNG, NSPasteboardTypeTIFF
16
+ from Foundation import NSData
17
+
18
+
19
+ # Temp directory for clipboard images
20
+ CLIPBOARD_TEMP_DIR = Path("/tmp/screenfix/clipboard")
21
+
22
+
23
+ class ClipboardWatcher:
24
+ """Watches clipboard for new images."""
25
+
26
+ def __init__(self, callback: Callable[[str], None], poll_interval: float = 0.3):
27
+ """
28
+ Initialize the clipboard watcher.
29
+
30
+ Args:
31
+ callback: Function to call when new image detected.
32
+ Called with path to saved image file.
33
+ poll_interval: How often to check clipboard (seconds).
34
+ """
35
+ self._callback = callback
36
+ self._poll_interval = poll_interval
37
+ self._running = False
38
+ self._thread: Optional[threading.Thread] = None
39
+ self._last_change_count = 0
40
+
41
+ def start(self):
42
+ """Start watching the clipboard."""
43
+ if self._running:
44
+ return
45
+
46
+ # Ensure temp directory exists
47
+ CLIPBOARD_TEMP_DIR.mkdir(parents=True, exist_ok=True)
48
+
49
+ # Get initial change count
50
+ pasteboard = NSPasteboard.generalPasteboard()
51
+ self._last_change_count = pasteboard.changeCount()
52
+
53
+ self._running = True
54
+ self._thread = threading.Thread(target=self._watch_loop, daemon=True)
55
+ self._thread.start()
56
+
57
+ def stop(self):
58
+ """Stop watching the clipboard."""
59
+ self._running = False
60
+ if self._thread:
61
+ self._thread.join(timeout=2.0)
62
+ self._thread = None
63
+
64
+ @property
65
+ def is_running(self) -> bool:
66
+ """Check if watcher is running."""
67
+ return self._running
68
+
69
+ def _watch_loop(self):
70
+ """Main watch loop running in background thread."""
71
+ while self._running:
72
+ try:
73
+ self._check_clipboard()
74
+ except Exception as e:
75
+ import sys
76
+ print(f"Clipboard watch error: {e}", file=sys.stderr)
77
+
78
+ time.sleep(self._poll_interval)
79
+
80
+ def _check_clipboard(self):
81
+ """Check if clipboard has new image data."""
82
+ pasteboard = NSPasteboard.generalPasteboard()
83
+ current_count = pasteboard.changeCount()
84
+
85
+ # No change
86
+ if current_count == self._last_change_count:
87
+ return
88
+
89
+ self._last_change_count = current_count
90
+
91
+ # Check for image data (PNG preferred, fallback to TIFF)
92
+ image_data = None
93
+ extension = ".png"
94
+
95
+ # Try PNG first
96
+ png_data = pasteboard.dataForType_(NSPasteboardTypePNG)
97
+ if png_data:
98
+ image_data = png_data
99
+ extension = ".png"
100
+ else:
101
+ # Try TIFF (screenshots are often TIFF on clipboard)
102
+ tiff_data = pasteboard.dataForType_(NSPasteboardTypeTIFF)
103
+ if tiff_data:
104
+ image_data = tiff_data
105
+ extension = ".tiff"
106
+
107
+ if not image_data:
108
+ return # No image on clipboard
109
+
110
+ # Save to temp file
111
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
112
+ filename = f"clipboard_{timestamp}{extension}"
113
+ temp_path = CLIPBOARD_TEMP_DIR / filename
114
+
115
+ # Write the image data
116
+ image_data.writeToFile_atomically_(str(temp_path), True)
117
+
118
+ # Clear clipboard to prevent re-triggers
119
+ pasteboard.clearContents()
120
+
121
+ # Convert TIFF to PNG if needed (for consistency)
122
+ if extension == ".tiff":
123
+ png_path = temp_path.with_suffix(".png")
124
+ if self._convert_to_png(temp_path, png_path):
125
+ os.remove(temp_path)
126
+ temp_path = png_path
127
+
128
+ # Trigger callback
129
+ self._callback(str(temp_path))
130
+
131
+ def _convert_to_png(self, tiff_path: Path, png_path: Path) -> bool:
132
+ """Convert TIFF to PNG using CoreGraphics."""
133
+ try:
134
+ from Quartz import (
135
+ CGImageSourceCreateWithURL,
136
+ CGImageSourceCreateImageAtIndex,
137
+ CGImageDestinationCreateWithURL,
138
+ CGImageDestinationAddImage,
139
+ CGImageDestinationFinalize,
140
+ kCGImagePropertyOrientation,
141
+ )
142
+ from Foundation import NSURL
143
+
144
+ # Read TIFF
145
+ source_url = NSURL.fileURLWithPath_(str(tiff_path))
146
+ source = CGImageSourceCreateWithURL(source_url, None)
147
+ if not source:
148
+ return False
149
+
150
+ image = CGImageSourceCreateImageAtIndex(source, 0, None)
151
+ if not image:
152
+ return False
153
+
154
+ # Write PNG
155
+ dest_url = NSURL.fileURLWithPath_(str(png_path))
156
+ destination = CGImageDestinationCreateWithURL(dest_url, "public.png", 1, None)
157
+ if not destination:
158
+ return False
159
+
160
+ CGImageDestinationAddImage(destination, image, None)
161
+ return CGImageDestinationFinalize(destination)
162
+
163
+ except Exception:
164
+ return False
@@ -0,0 +1,66 @@
1
+ """Configuration management for ScreenFix."""
2
+
3
+ import os
4
+ import json
5
+ from pathlib import Path
6
+
7
+ DEFAULT_CONFIG = {
8
+ "save_directory": "./screenfix/screenshots",
9
+ "tasks_file": "./screenfix/tasks/screenfix-tasks.md",
10
+ }
11
+
12
+ CONFIG_DIR = Path.home() / ".config" / "screenfix"
13
+ CONFIG_FILE = CONFIG_DIR / "config.json"
14
+
15
+
16
+ class Config:
17
+ """Configuration manager for ScreenFix."""
18
+
19
+ def __init__(self):
20
+ self._config = DEFAULT_CONFIG.copy()
21
+ self._load()
22
+
23
+ def _load(self):
24
+ """Load configuration from file."""
25
+ if CONFIG_FILE.exists():
26
+ try:
27
+ with open(CONFIG_FILE, "r") as f:
28
+ saved = json.load(f)
29
+ self._config.update(saved)
30
+ except (json.JSONDecodeError, IOError):
31
+ pass
32
+
33
+ def save(self):
34
+ """Save configuration to file."""
35
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
36
+ with open(CONFIG_FILE, "w") as f:
37
+ json.dump(self._config, f, indent=2)
38
+
39
+ @property
40
+ def save_directory(self) -> str:
41
+ """Directory where screenshots are saved."""
42
+ return self._config["save_directory"]
43
+
44
+ @save_directory.setter
45
+ def save_directory(self, value: str):
46
+ self._config["save_directory"] = os.path.expanduser(value)
47
+ self.save()
48
+
49
+ @property
50
+ def tasks_file(self) -> str:
51
+ """Path to the tasks.md file."""
52
+ return self._config["tasks_file"]
53
+
54
+ @tasks_file.setter
55
+ def tasks_file(self, value: str):
56
+ self._config["tasks_file"] = os.path.expanduser(value)
57
+ self.save()
58
+
59
+ def ensure_directories(self):
60
+ """Ensure save and tasks directories exist."""
61
+ Path(self.save_directory).mkdir(parents=True, exist_ok=True)
62
+ Path(self.tasks_file).parent.mkdir(parents=True, exist_ok=True)
63
+
64
+
65
+ # Global config instance
66
+ config = Config()
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ ScreenFix Daemon - Clipboard watcher for instant screenshot capture.
4
+
5
+ Watches the clipboard for new images and shows annotation window instantly.
6
+ Use Cmd+Ctrl+Shift+4 to capture screenshot to clipboard (no preview delay).
7
+
8
+ Run with: python -m screenfix.daemon
9
+ """
10
+
11
+ import json
12
+ import os
13
+ import queue
14
+ import signal
15
+ import sys
16
+ from datetime import datetime
17
+ from pathlib import Path
18
+
19
+ from AppKit import (
20
+ NSApplication,
21
+ NSApplicationActivationPolicyAccessory,
22
+ NSEventMaskAny,
23
+ )
24
+ from Foundation import NSDate, NSDefaultRunLoopMode
25
+
26
+ from .config import config
27
+
28
+
29
+ # State file for communication with MCP server
30
+ STATE_FILE = Path.home() / ".config" / "screenfix" / "state.json"
31
+
32
+
33
+ def update_state(listening: bool = None, last_capture: str = None):
34
+ """Update the state file for MCP server to read."""
35
+ STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
36
+
37
+ state = {"pid": os.getpid(), "listening": False}
38
+
39
+ if STATE_FILE.exists():
40
+ try:
41
+ with open(STATE_FILE, "r") as f:
42
+ state = json.load(f)
43
+ except (json.JSONDecodeError, IOError):
44
+ pass
45
+
46
+ state["pid"] = os.getpid()
47
+ if listening is not None:
48
+ state["listening"] = listening
49
+ if last_capture is not None:
50
+ state["last_capture"] = last_capture
51
+ state["last_capture_time"] = datetime.now().isoformat()
52
+
53
+ with open(STATE_FILE, "w") as f:
54
+ json.dump(state, f, indent=2)
55
+
56
+
57
+ def clear_state():
58
+ """Clear the state file on exit."""
59
+ if STATE_FILE.exists():
60
+ try:
61
+ os.remove(STATE_FILE)
62
+ except OSError:
63
+ pass
64
+
65
+
66
+ class ScreenFixDaemon:
67
+ """Daemon that watches clipboard for screenshots."""
68
+
69
+ def __init__(self):
70
+ self._running = False
71
+ self._clipboard_watcher = None
72
+ self._app = None
73
+ self._main_thread_queue = queue.Queue()
74
+
75
+ def _on_clipboard_image(self, image_path: str):
76
+ """Called when a new image is detected on clipboard."""
77
+ print(f"Screenshot detected: {image_path}", file=sys.stderr)
78
+ self._main_thread_queue.put(image_path)
79
+
80
+ def _process_main_thread_queue(self):
81
+ """Process any pending work on the main thread."""
82
+ try:
83
+ while True:
84
+ image_path = self._main_thread_queue.get_nowait()
85
+ self._show_annotation(image_path)
86
+ except queue.Empty:
87
+ pass
88
+
89
+ def _show_annotation(self, image_path: str):
90
+ """Show the annotation window. Must be called from main thread."""
91
+ from .annotation_window import show_annotation_window
92
+ show_annotation_window(image_path)
93
+
94
+ def _setup_signal_handlers(self):
95
+ """Set up signal handlers for graceful shutdown."""
96
+ def handle_signal(signum, frame):
97
+ print(f"\nShutting down...", file=sys.stderr)
98
+ self.stop()
99
+
100
+ signal.signal(signal.SIGINT, handle_signal)
101
+ signal.signal(signal.SIGTERM, handle_signal)
102
+
103
+ def start(self):
104
+ """Start the daemon."""
105
+ if self._running:
106
+ return
107
+
108
+ self._running = True
109
+ config.ensure_directories()
110
+
111
+ # Initialize NSApplication for UI
112
+ self._app = NSApplication.sharedApplication()
113
+ self._app.setActivationPolicy_(NSApplicationActivationPolicyAccessory)
114
+ self._app.finishLaunching()
115
+
116
+ self._setup_signal_handlers()
117
+
118
+ print("ScreenFix daemon started", file=sys.stderr)
119
+ print(f"Screenshots will be saved to: {config.save_directory}", file=sys.stderr)
120
+ print(f"Tasks will be saved to: {config.tasks_file}", file=sys.stderr)
121
+ print("\nUse Cmd+Ctrl+Shift+4 to capture (INSTANT, no delay!)", file=sys.stderr)
122
+ print("Press Ctrl+C to stop.\n", file=sys.stderr)
123
+
124
+ # Start clipboard watcher
125
+ from .clipboard_watcher import ClipboardWatcher
126
+ self._clipboard_watcher = ClipboardWatcher(self._on_clipboard_image)
127
+ self._clipboard_watcher.start()
128
+
129
+ update_state(listening=True)
130
+
131
+ # Run the main loop
132
+ while self._running:
133
+ self._process_main_thread_queue()
134
+
135
+ while True:
136
+ event = self._app.nextEventMatchingMask_untilDate_inMode_dequeue_(
137
+ NSEventMaskAny,
138
+ NSDate.dateWithTimeIntervalSinceNow_(0.05),
139
+ NSDefaultRunLoopMode,
140
+ True
141
+ )
142
+ if event is None:
143
+ break
144
+ self._app.sendEvent_(event)
145
+
146
+ def stop(self):
147
+ """Stop the daemon."""
148
+ self._running = False
149
+
150
+ if self._clipboard_watcher:
151
+ self._clipboard_watcher.stop()
152
+ self._clipboard_watcher = None
153
+
154
+ clear_state()
155
+
156
+ if self._app:
157
+ self._app.terminate_(None)
158
+
159
+
160
+ def is_daemon_running() -> bool:
161
+ """Check if the daemon is already running."""
162
+ if not STATE_FILE.exists():
163
+ return False
164
+
165
+ try:
166
+ with open(STATE_FILE, "r") as f:
167
+ state = json.load(f)
168
+
169
+ pid = state.get("pid")
170
+ if pid:
171
+ try:
172
+ os.kill(pid, 0)
173
+ return True
174
+ except OSError:
175
+ clear_state()
176
+ return False
177
+ except (json.JSONDecodeError, IOError):
178
+ return False
179
+
180
+ return False
181
+
182
+
183
+ def main():
184
+ """Main entry point."""
185
+ if is_daemon_running():
186
+ print("ScreenFix daemon is already running!", file=sys.stderr)
187
+ sys.exit(1)
188
+
189
+ daemon = ScreenFixDaemon()
190
+
191
+ try:
192
+ daemon.start()
193
+ except KeyboardInterrupt:
194
+ pass
195
+ except Exception as e:
196
+ print(f"Error: {e}", file=sys.stderr)
197
+ import traceback
198
+ traceback.print_exc()
199
+ finally:
200
+ daemon.stop()
201
+
202
+
203
+ if __name__ == "__main__":
204
+ main()