|
1 | 1 | import os
|
2 |
| -import time |
3 |
| -import asyncio |
4 |
| -import pyperclip |
5 |
| -from dotenv import load_dotenv |
6 |
| -from groq import AsyncGroq |
7 |
| -import pystray |
8 |
| -from pystray import MenuItem as item |
9 |
| -from PIL import Image |
10 |
| -from plyer import notification |
11 | 2 | import threading
|
12 |
| -from typing import Dict |
13 |
| -from commands import commands |
14 |
| -import sys |
15 |
| -from src.config import ( |
16 |
| - APP_NAME, |
17 |
| - APP_ICON, |
18 |
| - TRAY_ICON, |
19 |
| - NOTIFICATION_TIMEOUT_SHORT, |
20 |
| - NOTIFICATION_TIMEOUT_LONG, |
21 |
| -) |
22 |
| - |
23 |
| -if sys.platform.startswith("win"): |
24 |
| - import win32event |
25 |
| - import win32api |
26 |
| - import winerror |
27 |
| -else: |
28 |
| - import fcntl |
29 |
| - |
30 |
| - |
31 |
| -class ClipboardListener: |
32 |
| - def __init__(self): |
33 |
| - self.load_env_vars() |
34 |
| - self.last_called = 0 |
35 |
| - self.client = AsyncGroq(api_key=self.GROQ_API_KEY) |
36 |
| - self.commands = self.load_commands() |
37 |
| - self.ensure_single_instance() |
38 |
| - |
39 |
| - def load_env_vars(self) -> None: |
40 |
| - load_dotenv() |
41 |
| - self.GROQ_API_KEY = os.getenv("GROQ_API_KEY") |
42 |
| - self.GROQ_MODEL = os.getenv("GROQ_MODEL") |
43 |
| - |
44 |
| - def load_commands(self) -> Dict[str, str]: |
45 |
| - notification.notify( |
46 |
| - title=APP_NAME, |
47 |
| - message="Gramma is now running in the background!", |
48 |
| - app_name=APP_NAME, |
49 |
| - app_icon=APP_ICON, |
50 |
| - timeout=NOTIFICATION_TIMEOUT_LONG, |
51 |
| - ) |
52 |
| - return commands |
53 |
| - |
54 |
| - async def process_command(self, command: str, user_msg: str) -> None: |
55 |
| - if time.time() - self.last_called < 4: |
56 |
| - self.notify( |
57 |
| - "Rate Limit", |
58 |
| - "Please wait a few seconds before trying again.", |
59 |
| - NOTIFICATION_TIMEOUT_LONG, |
60 |
| - ) |
61 |
| - return |
62 |
| - |
63 |
| - prompt = self.commands.get(f"PROMPT_{command.upper()}") |
64 |
| - if not prompt: |
65 |
| - return |
66 |
| - |
67 |
| - try: |
68 |
| - completion = await self.client.chat.completions.create( |
69 |
| - messages=[ |
70 |
| - {"role": "system", "content": prompt}, |
71 |
| - {"role": "user", "content": user_msg}, |
72 |
| - ], |
73 |
| - model=self.GROQ_MODEL, |
74 |
| - temperature=0.5, |
75 |
| - max_tokens=1024, |
76 |
| - top_p=1, |
77 |
| - stop=None, |
78 |
| - stream=False, |
79 |
| - ) |
80 |
| - |
81 |
| - response_msg = completion.choices[0].message.content |
82 |
| - pyperclip.copy(response_msg) |
83 |
| - self.notify( |
84 |
| - "Done!", |
85 |
| - "Text processed and copied to clipboard.", |
86 |
| - NOTIFICATION_TIMEOUT_SHORT, |
87 |
| - ) |
88 |
| - self.last_called = time.time() |
89 |
| - except Exception as e: |
90 |
| - self.notify("Error", f"An error occurred: {e}", NOTIFICATION_TIMEOUT_LONG) |
91 |
| - |
92 |
| - def process_command_sync(self, command: str, user_msg: str) -> None: |
93 |
| - asyncio.run(self.process_command(command, user_msg)) |
94 |
| - |
95 |
| - def notify(self, title: str, message: str, timeout: int) -> None: |
96 |
| - notification.notify( |
97 |
| - title=title, |
98 |
| - message=message, |
99 |
| - app_name=APP_NAME, |
100 |
| - app_icon=APP_ICON, |
101 |
| - timeout=timeout, |
102 |
| - ) |
103 |
| - |
104 |
| - def monitor_clipboard(self) -> None: |
105 |
| - recent_value = pyperclip.paste() |
106 |
| - while True: |
107 |
| - time.sleep(0.1) |
108 |
| - tmp_value = pyperclip.paste() |
109 |
| - if tmp_value != recent_value: |
110 |
| - recent_value = tmp_value |
111 |
| - for prefix in self.commands: |
112 |
| - if tmp_value.startswith(f"!{prefix.lower()[7:]}"): |
113 |
| - command = tmp_value.split(" ")[0][1:] |
114 |
| - user_msg = tmp_value[len(command) + 2 :].strip() |
115 |
| - self.process_command_sync(command, user_msg) |
116 |
| - |
117 |
| - def setup_tray_icon(self) -> None: |
118 |
| - image = Image.open(TRAY_ICON) |
119 |
| - icon = pystray.Icon( |
120 |
| - APP_NAME, |
121 |
| - image, |
122 |
| - APP_NAME, |
123 |
| - menu=pystray.Menu(item("Exit", self.exit_program)), |
124 |
| - ) |
125 |
| - icon.run() |
126 |
| - |
127 |
| - def exit_program(self, icon) -> None: |
128 |
| - icon.stop() |
| 3 | +from src.data import SharedData |
| 4 | +from src.core.clipboard_listener import ClipboardListener |
| 5 | +from src.core.tray_icon import TrayIcon |
| 6 | +from src.managers.instance_manager import InstanceManager |
| 7 | + |
| 8 | + |
| 9 | +def main(): |
| 10 | + instance_manager = InstanceManager() |
| 11 | + shared_data = SharedData() |
| 12 | + listener = ClipboardListener(shared_data) |
| 13 | + TrayIcon(listener) |
| 14 | + |
| 15 | + try: |
| 16 | + listener_thread = threading.Thread(target=listener.monitor_clipboard) |
| 17 | + listener_thread.start() |
| 18 | + except KeyboardInterrupt: |
| 19 | + listener.stop_monitoring() |
| 20 | + except Exception as e: |
| 21 | + print(f"An error occurred: {e}") |
| 22 | + listener.stop_monitoring() |
| 23 | + finally: |
| 24 | + instance_manager.cleanup() |
129 | 25 | os._exit(0)
|
130 | 26 |
|
131 |
| - def ensure_single_instance(self) -> None: |
132 |
| - if sys.platform.startswith("win"): |
133 |
| - self.handle_mutex() |
134 |
| - else: |
135 |
| - self.handle_lock_file() |
136 |
| - |
137 |
| - def handle_mutex(self) -> None: |
138 |
| - self.mutex = None |
139 |
| - try: |
140 |
| - self.mutex = win32event.CreateMutex(None, False, APP_NAME) |
141 |
| - if win32api.GetLastError() == winerror.ERROR_ALREADY_EXISTS: |
142 |
| - self.notify_instance_already_running() |
143 |
| - except Exception as e: |
144 |
| - print(f"Error creating mutex: {e}") |
145 |
| - os._exit(1) |
146 |
| - |
147 |
| - def handle_lock_file(self) -> None: |
148 |
| - lock_file = "/tmp/clipboardlistener.lock" |
149 |
| - try: |
150 |
| - self.lock_fd = os.open(lock_file, os.O_CREAT | os.O_EXCL | os.O_RDWR) |
151 |
| - except OSError: |
152 |
| - self.notify_instance_already_running() |
153 |
| - os._exit(1) |
154 |
| - else: |
155 |
| - fcntl.flock(self.lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) |
156 |
| - |
157 | 27 |
|
158 | 28 | if __name__ == "__main__":
|
159 |
| - listener = ClipboardListener() |
160 |
| - threading.Thread(target=listener.setup_tray_icon).start() |
161 |
| - listener.monitor_clipboard() |
162 |
| - |
163 |
| - if sys.platform.startswith("win"): |
164 |
| - win32event.ReleaseMutex(listener.mutex) |
165 |
| - else: |
166 |
| - os.close(listener.lock_fd) |
167 |
| - os.unlink("/tmp/clipboardlistener.lock") |
| 29 | + main() |
0 commit comments