diff --git a/SUPERCOPY.py b/SUPERCOPY.py index 2c29083..e7459ee 100644 --- a/SUPERCOPY.py +++ b/SUPERCOPY.py @@ -1,5 +1,6 @@ files = [ "core/app_launcher.py", + "core/config.py", "core/discord_presence.py", "core/dukto.py", "core/file_search.py", diff --git a/core/app_launcher.py b/core/app_launcher.py index b2dc3c7..bbc8399 100644 --- a/core/app_launcher.py +++ b/core/app_launcher.py @@ -193,14 +193,9 @@ def launch(app: App): return try: - # Using ShellExecute is more robust for launching various application types on Windows, - # as it leverages the Windows shell's own mechanisms. This is particularly helpful for - # non-standard executables like PWAs or Microsoft Store apps. command_parts = shlex.split(app.exec, posix=False) target = command_parts[0] - # Use subprocess.list2cmdline to correctly re-assemble the arguments string, - # preserving quotes around arguments with spaces. arguments = subprocess.list2cmdline(command_parts[1:]) win32api.ShellExecute( diff --git a/core/config.py b/core/config.py new file mode 100644 index 0000000..c02cf69 --- /dev/null +++ b/core/config.py @@ -0,0 +1,82 @@ +import json +from pathlib import Path +from typing import Any, Dict +import platform + +class Config: + DEFAULT_CONFIG = { + "hotkey": "super", + "discord_presence": True, + "auto_update": True, + "http_share_port": 8080, + "dukto_udp_port": 4644, + "dukto_tcp_port": 4644, + "search_engine": "brave" + } + + def __init__(self): + self.config_dir = self._get_config_dir() + self.config_file = self.config_dir / "config.json" + self.config_data = self._load_config() + + def _get_config_dir(self) -> Path: + system = platform.system() + + if system == "Windows": + base = Path.home() / "AppData" / "Roaming" + else: + base = Path.home() / ".config" + + config_dir = base / "CLARA" + config_dir.mkdir(parents=True, exist_ok=True) + return config_dir + + def _load_config(self) -> Dict[str, Any]: + system = platform.system() + + if self.config_file.exists(): + try: + with open(self.config_file, 'r', encoding='utf-8') as f: + loaded = json.load(f) + # Merge with defaults for updates + config = self.DEFAULT_CONFIG.copy() + config.update(loaded) + return config + except (json.JSONDecodeError, IOError) as e: + print(f"Error loading config: {e}. Using defaults.") + return self.DEFAULT_CONFIG.copy() + else: + # default config file + conf = self.DEFAULT_CONFIG.copy() + if system == "Windows": + conf["hotkey"] = "ctrl+space" + else: + conf["hotkey"] = "super" + + self._save_config(self.DEFAULT_CONFIG) + return self.DEFAULT_CONFIG.copy() + + def _save_config(self, config_data: Dict[str, Any]): + try: + with open(self.config_file, 'w', encoding='utf-8') as f: + json.dump(config_data, f, indent=4) + except IOError as e: + print(f"Error saving config: {e}") + + def get(self, key: str, default: Any = None) -> Any: + return self.config_data.get(key, default) + + def set(self, key: str, value: Any): + self.config_data[key] = value + self._save_config(self.config_data) + + def get_all(self) -> Dict[str, Any]: + return self.config_data.copy() + + def reset(self): + self.config_data = self.DEFAULT_CONFIG.copy() + self._save_config(self.config_data) + + +# Global config instance +config = Config() \ No newline at end of file diff --git a/main.py b/main.py index aaa18bc..bb99e26 100644 --- a/main.py +++ b/main.py @@ -8,6 +8,7 @@ from core.discord_presence import presence from core.dukto import DuktoProtocol from core.updater import update_repository, is_update_available from core.app_launcher import list_apps +from core.config import config from windows.main_window import MainWindow @@ -40,7 +41,7 @@ def main(): super_menu = not "--no-super" in sys.argv and not "--no-start" in sys.argv noupdate = "--no-update" in sys.argv - if not noupdate: + if not noupdate and config.get("auto_update", True): update_available = is_update_available() if update_available: update_repository() @@ -50,10 +51,22 @@ def main(): preload_thread.start() dukto_handler = DuktoProtocol() + dukto_handler.set_ports( + udp_port=config.get("dukto_udp_port", 4644), + tcp_port=config.get("dukto_tcp_port", 4644) + ) - pet = MainWindow(dukto_handler=dukto_handler, strings=strings, restart=restart, no_quit=no_quit, super_menu=super_menu) + pet = MainWindow( + dukto_handler=dukto_handler, + strings=strings, + config=config, + restart=restart, + no_quit=no_quit, + super_menu=super_menu + ) - presence.start() + if config.get("discord_presence", True): + presence.start() dukto_handler.initialize() dukto_handler.say_hello() diff --git a/windows/file_search.py b/windows/file_search.py index 96d09b6..3b8f818 100644 --- a/windows/file_search.py +++ b/windows/file_search.py @@ -23,4 +23,4 @@ class FileSearchResults(QtWidgets.QDialog): if os.path.exists(file_path): directory = os.path.dirname(file_path) url = QtCore.QUrl.fromLocalFile(directory) - QtGui.QDesktopServices.openUrl(url) + QtGui.QDesktopServices.openUrl(url) \ No newline at end of file diff --git a/windows/main_window.py b/windows/main_window.py index acdf45a..ec82f45 100644 --- a/windows/main_window.py +++ b/windows/main_window.py @@ -10,6 +10,7 @@ from core.dukto import Peer from core.file_search import find from core.web_search import MullvadLetaWrapper from core.http_share import FileShareServer +from core.config import Config from windows.app_launcher import AppLauncherDialog from windows.file_search import FileSearchResults @@ -38,10 +39,12 @@ class MainWindow(QtWidgets.QMainWindow): http_download_signal = QtCore.Signal(str, str) - def __init__(self, dukto_handler, strings, restart=False, no_quit=False, super_menu=True): + def __init__(self, dukto_handler, strings, config: Config, restart=False, no_quit=False, super_menu=True): super().__init__() self.strings = strings + self.config = config + self.listener = None flags = ( QtCore.Qt.FramelessWindowHint #type: ignore @@ -69,7 +72,8 @@ class MainWindow(QtWidgets.QMainWindow): self.progress_dialog = None # HTTP file sharing - self.http_share = FileShareServer(port=8080) + http_port = self.config.get("http_share_port", 8080) + self.http_share = FileShareServer(port=http_port) self.http_share.on_download = lambda filename, ip: self.http_download_signal.emit(filename, ip) # Connect Dukto callbacks to emit signals @@ -109,7 +113,8 @@ class MainWindow(QtWidgets.QMainWindow): # Super key self.show_menu_signal.connect(self.show_menu) - self.start_hotkey_listener() + if self.super_menu: + self.start_hotkey_listener() def build_menus(self): s = self.strings["main_window"]["right_menu"] @@ -200,17 +205,40 @@ class MainWindow(QtWidgets.QMainWindow): def show_menu(self): self.left_menu.popup(QtGui.QCursor.pos()) - def on_press(self, key): - if self.super_menu: - if key == keyboard.Key.cmd: - self.show_menu_signal.emit() - def start_hotkey_listener(self): - self.listener = keyboard.Listener(on_press=self.on_press) - self.listener.start() + hotkey_str = self.config.get("hotkey", "super") + + def on_activate(): + self.show_menu_signal.emit() + + key_map = { + "super": "cmd", + "ctrl": "ctrl", + "space": "space", + } + + try: + keys = [key_map.get(k.strip().lower(), k.strip().lower()) for k in hotkey_str.split('+')] + formatted_hotkey = '<' + '>+<'.join(keys) + '>' #type: ignore + + hotkey = keyboard.HotKey( + keyboard.HotKey.parse(formatted_hotkey), + on_activate + ) + + self.listener = keyboard.Listener( + on_press=hotkey.press, #type: ignore + on_release=hotkey.release #type: ignore + ) + self.listener.start() + + except Exception as e: + print(f"Failed to set hotkey '{hotkey_str}': {e}. Hotkey will be disabled.") + self.listener = None def closeEvent(self, event): - self.listener.stop() + if self.listener: + self.listener.stop() if self.http_share.is_running(): self.http_share.stop() super().closeEvent(event) @@ -518,7 +546,8 @@ class MainWindow(QtWidgets.QMainWindow): if ok and query: try: QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) #type: ignore - leta = MullvadLetaWrapper(engine="brave") + search_engine = self.config.get("search_engine", "brave") + leta = MullvadLetaWrapper(engine=search_engine) results = leta.search(query) if results and results.get('results'):