From 751e0a9d7cd3db426fa1292bd66931869f53ef25 Mon Sep 17 00:00:00 2001 From: Viet-Anh Nguyen Date: Mon, 30 Sep 2024 22:50:48 +0700 Subject: [PATCH] Fix shortcut handling on macOS --- llama_assistant/config.py | 11 ++++- llama_assistant/global_hotkey.py | 9 +++- llama_assistant/llama_assistant.py | 63 +++++++++++++++++++++------- llama_assistant/setting_dialog.py | 26 ++++++++---- llama_assistant/shortcut_recorder.py | 33 ++++++++++++++- pyproject.toml | 2 +- 6 files changed, 114 insertions(+), 30 deletions(-) diff --git a/llama_assistant/config.py b/llama_assistant/config.py index 219045e..fb56c59 100644 --- a/llama_assistant/config.py +++ b/llama_assistant/config.py @@ -1,7 +1,16 @@ import json from pathlib import Path - +DEFAULT_LAUNCH_SHORTCUT = "++" +DEFAULT_SETTINGS = { + "shortcut": DEFAULT_LAUNCH_SHORTCUT, + "color": "#1E1E1E", + "transparency": 95, + "text_model": "hugging-quants/Llama-3.2-1B-Instruct-Q4_K_M-GGUF", + "multimodal_model": "vikhyatk/moondream2", + "hey_llama_chat": False, + "hey_llama_mic": False, +} DEFAULT_MODELS = [ { "model_name": "Llama-3.2-1B-Instruct-Q4_K_M-GGUF", diff --git a/llama_assistant/global_hotkey.py b/llama_assistant/global_hotkey.py index 3c97a98..aa54da0 100644 --- a/llama_assistant/global_hotkey.py +++ b/llama_assistant/global_hotkey.py @@ -1,13 +1,20 @@ from PyQt6.QtCore import QObject, pyqtSignal from pynput import keyboard +from llama_assistant.config import DEFAULT_LAUNCH_SHORTCUT + class GlobalHotkey(QObject): activated = pyqtSignal() def __init__(self, hotkey): super().__init__() - self.hotkey = keyboard.HotKey(keyboard.HotKey.parse(hotkey), self.on_activate) + try: + self.hotkey = keyboard.HotKey(keyboard.HotKey.parse(hotkey), self.on_activate) + except ValueError: + self.hotkey = keyboard.HotKey( + keyboard.HotKey.parse(DEFAULT_LAUNCH_SHORTCUT), self.on_activate + ) self.listener = keyboard.Listener( on_press=self.for_canonical(self.hotkey.press), on_release=self.for_canonical(self.hotkey.release), diff --git a/llama_assistant/llama_assistant.py b/llama_assistant/llama_assistant.py index b010bf4..9bdaa89 100644 --- a/llama_assistant/llama_assistant.py +++ b/llama_assistant/llama_assistant.py @@ -1,6 +1,9 @@ import json +import copy +import time from importlib import resources from pathlib import Path +import traceback from PyQt6.QtWidgets import ( QApplication, @@ -14,6 +17,7 @@ QMenu, QLabel, QScrollArea, + QMessageBox, ) from PyQt6.QtCore import ( Qt, @@ -37,6 +41,7 @@ QTextCursor, ) +from llama_assistant import config from llama_assistant.wake_word_detector import WakeWordDetector from llama_assistant.custom_plaintext_editor import CustomPlainTextEdit from llama_assistant.global_hotkey import GlobalHotkey @@ -118,21 +123,13 @@ def load_settings(self): with open(settings_file, "r") as f: self.settings = json.load(f) self.settings["text_model"] = self.settings.get( - "text_model", "hugging-quants/Llama-3.2-1B-Instruct-Q4_K_M-GGUF" + "text_model", config.DEFAULT_SETTINGS["text_model"] ) self.settings["multimodal_model"] = self.settings.get( - "multimodal_model", "vikhyatk/moondream2" + "multimodal_model", config.DEFAULT_SETTINGS["multimodal_model"] ) else: - self.settings = { - "shortcut": "++", - "color": "#1E1E1E", - "transparency": 90, - "text_model": "hugging-quants/Llama-3.2-1B-Instruct-Q4_K_M-GGUF", - "multimodal_model": "vikhyatk/moondream2", - "hey_llama_chat": False, - "hey_llama_mic": False, - } + self.settings = copy.deepcopy(config.DEFAULT_SETTINGS) self.save_settings() if self.settings.get("hey_llama_chat", False) and self.wake_word_detector is None: self.init_wake_word_detector() @@ -142,10 +139,21 @@ def load_settings(self): self.current_multimodal_model = self.settings.get("multimodal_model") def setup_global_shortcut(self): - if hasattr(self, "global_hotkey"): - self.global_hotkey.stop() - self.global_hotkey = GlobalHotkey(self.settings["shortcut"]) - self.global_hotkey.activated.connect(self.toggle_visibility) + try: + if hasattr(self, "global_hotkey"): + self.global_hotkey.stop() + time.sleep(0.1) # Give a short delay to ensure the previous listener has stopped + try: + self.global_hotkey = GlobalHotkey(self.settings["shortcut"]) + self.global_hotkey.activated.connect(self.toggle_visibility) + except Exception as e: + print(f"Error setting up global shortcut: {e}") + # Fallback to default shortcut if there's an error + self.global_hotkey = GlobalHotkey(config.DEFAULT_LAUNCH_SHORTCUT) + self.global_hotkey.activated.connect(self.toggle_visibility) + except Exception as e: + print(f"Error setting up global shortcut: {e}") + traceback.print_exc() def open_settings(self): dialog = SettingsDialog(self) @@ -158,7 +166,30 @@ def open_settings(self): self.update_styles() if old_shortcut != self.settings["shortcut"]: - self.setup_global_shortcut() + msg = QMessageBox() + msg.setIcon(QMessageBox.Icon.Information) + msg.setText("Global shortcut has been updated") + msg.setInformativeText( + "The changes will take effect after you restart the application." + ) + msg.setWindowTitle("Restart Required") + msg.setStandardButtons( + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + msg.button(QMessageBox.StandardButton.Yes).setText("Restart Now") + msg.button(QMessageBox.StandardButton.No).setText("Restart Later") + msg.setDefaultButton(QMessageBox.StandardButton.Yes) + + result = msg.exec() + + if result == QMessageBox.StandardButton.Yes: + self.restart_application() + else: + print("User chose to restart later.") + + def restart_application(self): + QApplication.quit() + # The application will restart automatically because it is being run from a script def save_settings(self): home_dir = Path.home() diff --git a/llama_assistant/setting_dialog.py b/llama_assistant/setting_dialog.py index 4e11e14..3efed5e 100644 --- a/llama_assistant/setting_dialog.py +++ b/llama_assistant/setting_dialog.py @@ -18,6 +18,7 @@ ) from PyQt6.QtCore import pyqtSignal, Qt from PyQt6.QtGui import QColor +from pynput import keyboard from llama_assistant.shortcut_recorder import ShortcutRecorder from llama_assistant import config @@ -156,7 +157,7 @@ def choose_color(self): self.color = color def reset_shortcut(self): - self.shortcut_recorder.setText("++") + self.shortcut_recorder.setText(config.DEFAULT_LAUNCH_SHORTCUT) def update_hey_llama_mic_state(self, state): self.hey_llama_mic_checkbox.setEnabled(state == Qt.CheckState.Checked.value) @@ -168,7 +169,12 @@ def load_settings(self): if settings_file.exists(): with open(settings_file, "r") as f: settings = json.load(f) - self.shortcut_recorder.setText(settings.get("shortcut", "++")) + try: + keyboard.HotKey(keyboard.HotKey.parse(settings["shortcut"]), lambda: None) + except ValueError: + settings["shortcut"] = config.DEFAULT_LAUNCH_SHORTCUT + self.save_settings(settings) + self.shortcut_recorder.setText(settings.get("shortcut", config.DEFAULT_LAUNCH_SHORTCUT)) self.color = QColor(settings.get("color", "#1E1E1E")) self.transparency_slider.setValue(int(settings.get("transparency", 90))) @@ -198,15 +204,17 @@ def get_settings(self): "hey_llama_mic": self.hey_llama_mic_checkbox.isChecked(), } - def save_settings(self): - home_dir = Path.home() - settings_dir = home_dir / "llama_assistant" - settings_file = settings_dir / "settings.json" + def save_settings(self, settings=None): + if settings is None: + home_dir = Path.home() + settings_dir = home_dir / "llama_assistant" + settings_file = settings_dir / "settings.json" + + if not settings_dir.exists(): + settings_dir.mkdir(parents=True) - if not settings_dir.exists(): - settings_dir.mkdir(parents=True) + settings = self.get_settings() - settings = self.get_settings() with open(settings_file, "w") as f: json.dump(settings, f) diff --git a/llama_assistant/shortcut_recorder.py b/llama_assistant/shortcut_recorder.py index 2456572..ea0be14 100644 --- a/llama_assistant/shortcut_recorder.py +++ b/llama_assistant/shortcut_recorder.py @@ -1,8 +1,14 @@ +import sys + from PyQt6.QtWidgets import QLineEdit from PyQt6.QtCore import Qt from PyQt6.QtGui import QKeyEvent, QKeySequence +def is_macos(): + return sys.platform == "darwin" + + class ShortcutRecorder(QLineEdit): def __init__(self, parent=None): super().__init__(parent) @@ -10,16 +16,39 @@ def __init__(self, parent=None): self.setPlaceholderText("Press a key combination") self.recorded_shortcut = None + # Set the style sheet for rounded corners + self.setStyleSheet( + """ + QLineEdit { + border: 1px solid #a0a0a0; + border-radius: 7.5px; + padding: 2px 5px; + background-color: #fefefe; + color: #333333; + font-size: 14px; + } + QLineEdit:focus { + border-color: #3498db; + } + """ + ) + def keyPressEvent(self, event: QKeyEvent): modifiers = [] if event.modifiers() & Qt.KeyboardModifier.ControlModifier: - modifiers.append("") + if is_macos(): + modifiers.append("") + else: + modifiers.append("") if event.modifiers() & Qt.KeyboardModifier.AltModifier: modifiers.append("") if event.modifiers() & Qt.KeyboardModifier.ShiftModifier: modifiers.append("") if event.modifiers() & Qt.KeyboardModifier.MetaModifier: - modifiers.append("") # Using for Meta/Command key + if is_macos(): + modifiers.append("") + else: + modifiers.append("") key = event.key() if key not in ( diff --git a/pyproject.toml b/pyproject.toml index 08c15d8..a3f6160 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "llama-assistant" -version = "0.1.22" +version = "0.1.23" authors = [ {name = "Viet-Anh Nguyen", email = "vietanh.dev@gmail.com"}, ]