Compare commits
4 Commits
790c9c3778
...
2c42c88555
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c42c88555 | ||
|
|
795cdb9daf | ||
|
|
b6becb6912 | ||
|
|
a6a83bf70e |
@@ -12,7 +12,7 @@ A WIP desktop assistant for Linux and Windows.
|
|||||||
- File search
|
- File search
|
||||||
- Web search
|
- Web search
|
||||||
- Updater
|
- Updater
|
||||||
- Super key menu (Doesn't work properly on Windows)
|
- Global menu (defaults: `Super` on Linux, `Ctrl+Space` on Windows) (you can disable it in the config)
|
||||||
- Local network file and text transfer (Dukto protocol)
|
- Local network file and text transfer (Dukto protocol)
|
||||||
- Browser based file and text transfer (HTTP)
|
- Browser based file and text transfer (HTTP)
|
||||||
- Discord Rich Presence integration
|
- Discord Rich Presence integration
|
||||||
@@ -20,7 +20,7 @@ A WIP desktop assistant for Linux and Windows.
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
- Python 3
|
- Python 3
|
||||||
- X11 Desktop
|
- X11 Desktop (Linux only)
|
||||||
- [`fd`](https://github.com/sharkdp/fd)
|
- [`fd`](https://github.com/sharkdp/fd)
|
||||||
- `git`
|
- `git`
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ A WIP desktop assistant for Linux and Windows.
|
|||||||
4. Launch `main.py`. Available options:
|
4. Launch `main.py`. Available options:
|
||||||
- `--restart` Add a restart option to the right click menu
|
- `--restart` Add a restart option to the right click menu
|
||||||
- `--no-quit` Hide the quit option
|
- `--no-quit` Hide the quit option
|
||||||
- `--no-super`/`--no-start` Disable the super key menu (recommended on Windows)
|
|
||||||
- `--no-update` Don't update automatically on startup
|
- `--no-update` Don't update automatically on startup
|
||||||
|
5. To configure, go to `~/.config/CLARA/config.json` on Linux and `~/AppData/Roaming/CLARA/config.json` on Windows.
|
||||||
|
|
||||||
If you want to contribute in any way, PRs (and issues) welcome
|
If you want to contribute in any way, PRs (and issues) welcome
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
files = [
|
files = [
|
||||||
"core/app_launcher.py",
|
"core/app_launcher.py",
|
||||||
|
"core/config.py",
|
||||||
"core/discord_presence.py",
|
"core/discord_presence.py",
|
||||||
"core/dukto.py",
|
"core/dukto.py",
|
||||||
"core/file_search.py",
|
"core/file_search.py",
|
||||||
|
|||||||
@@ -193,14 +193,9 @@ def launch(app: App):
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
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)
|
command_parts = shlex.split(app.exec, posix=False)
|
||||||
target = command_parts[0]
|
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:])
|
arguments = subprocess.list2cmdline(command_parts[1:])
|
||||||
|
|
||||||
win32api.ShellExecute(
|
win32api.ShellExecute(
|
||||||
|
|||||||
82
core/config.py
Normal file
82
core/config.py
Normal file
@@ -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()
|
||||||
19
main.py
19
main.py
@@ -8,6 +8,7 @@ from core.discord_presence import presence
|
|||||||
from core.dukto import DuktoProtocol
|
from core.dukto import DuktoProtocol
|
||||||
from core.updater import update_repository, is_update_available
|
from core.updater import update_repository, is_update_available
|
||||||
from core.app_launcher import list_apps
|
from core.app_launcher import list_apps
|
||||||
|
from core.config import config
|
||||||
|
|
||||||
from windows.main_window import MainWindow
|
from windows.main_window import MainWindow
|
||||||
|
|
||||||
@@ -37,10 +38,9 @@ def main():
|
|||||||
|
|
||||||
restart = "--restart" in sys.argv
|
restart = "--restart" in sys.argv
|
||||||
no_quit = "--no-quit" in sys.argv
|
no_quit = "--no-quit" in sys.argv
|
||||||
super_menu = not "--no-super" in sys.argv and not "--no-start" in sys.argv
|
|
||||||
noupdate = "--no-update" 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()
|
update_available = is_update_available()
|
||||||
if update_available:
|
if update_available:
|
||||||
update_repository()
|
update_repository()
|
||||||
@@ -50,10 +50,21 @@ def main():
|
|||||||
preload_thread.start()
|
preload_thread.start()
|
||||||
|
|
||||||
dukto_handler = DuktoProtocol()
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
presence.start()
|
if config.get("discord_presence", True):
|
||||||
|
presence.start()
|
||||||
|
|
||||||
dukto_handler.initialize()
|
dukto_handler.initialize()
|
||||||
dukto_handler.say_hello()
|
dukto_handler.say_hello()
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"share_files_submenu": "File(s)",
|
"share_files_submenu": "File(s)",
|
||||||
"share_text_submenu": "Text",
|
"share_text_submenu": "Text",
|
||||||
"via_browser": "Via Browser...",
|
"via_browser": "Via Browser...",
|
||||||
|
"settings": "Settings",
|
||||||
"check_updates": "Check for updates",
|
"check_updates": "Check for updates",
|
||||||
"restart": "Restart",
|
"restart": "Restart",
|
||||||
"toggle_visibility": "Hide/Show",
|
"toggle_visibility": "Hide/Show",
|
||||||
@@ -108,5 +109,18 @@
|
|||||||
"update_failed_title": "Update Failed",
|
"update_failed_title": "Update Failed",
|
||||||
"update_failed_text": "{message}"
|
"update_failed_text": "{message}"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"config_window": {
|
||||||
|
"title": "Settings",
|
||||||
|
"hotkey_label": "Global Hotkey:",
|
||||||
|
"discord_presence_label": "Enable Discord Presence:",
|
||||||
|
"auto_update_label": "Enable Auto-Update:",
|
||||||
|
"http_share_port_label": "HTTP Share Port:",
|
||||||
|
"dukto_udp_port_label": "Dukto UDP Port:",
|
||||||
|
"dukto_tcp_port_label": "Dukto TCP Port:",
|
||||||
|
"search_engine_label": "Web Search Engine:",
|
||||||
|
"restart_note": "Note: Some changes (like ports or hotkey) may require a restart to take effect.",
|
||||||
|
"reset_title": "Confirm Reset",
|
||||||
|
"reset_text": "Are you sure you want to reset all settings to their default values?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,6 +51,7 @@
|
|||||||
"share_files_submenu": "File(s)",
|
"share_files_submenu": "File(s)",
|
||||||
"share_text_submenu": "Message",
|
"share_text_submenu": "Message",
|
||||||
"via_browser": "Via Browser...",
|
"via_browser": "Via Browser...",
|
||||||
|
"settings": "Preferences",
|
||||||
"check_updates": "Check for Updates",
|
"check_updates": "Check for Updates",
|
||||||
"restart": "Restart",
|
"restart": "Restart",
|
||||||
"toggle_visibility": "Hide/Show",
|
"toggle_visibility": "Hide/Show",
|
||||||
@@ -108,5 +109,18 @@
|
|||||||
"update_failed_title": "Update Failed",
|
"update_failed_title": "Update Failed",
|
||||||
"update_failed_text": "{message}"
|
"update_failed_text": "{message}"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"config_window": {
|
||||||
|
"title": "Preferences",
|
||||||
|
"hotkey_label": "Global Hotkey:",
|
||||||
|
"discord_presence_label": "Show CLARA on your Discord status:",
|
||||||
|
"auto_update_label": "Update Automatically:",
|
||||||
|
"http_share_port_label": "Browser Sharing Port:",
|
||||||
|
"dukto_udp_port_label": "Dukto Discovery Port (UDP):",
|
||||||
|
"dukto_tcp_port_label": "Dukto Transfer Port (TCP):",
|
||||||
|
"search_engine_label": "Search Engine:",
|
||||||
|
"restart_note": "Just a heads-up: some changes (like ports or the hotkey) will need a restart to work.",
|
||||||
|
"reset_title": "Reset Settings?",
|
||||||
|
"reset_text": "Are you sure you want to go back to the default settings?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
88
windows/config_window.py
Normal file
88
windows/config_window.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
from PySide6 import QtWidgets
|
||||||
|
from core.config import Config
|
||||||
|
|
||||||
|
class ConfigWindow(QtWidgets.QDialog):
|
||||||
|
def __init__(self, strings, config: Config, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.strings = strings.get("config_window", {})
|
||||||
|
self.config = config
|
||||||
|
self.setWindowTitle(self.strings.get("title", "Settings"))
|
||||||
|
self.setMinimumWidth(400)
|
||||||
|
|
||||||
|
self.layout = QtWidgets.QVBoxLayout(self) # type: ignore
|
||||||
|
self.form_layout = QtWidgets.QFormLayout()
|
||||||
|
|
||||||
|
# Create widgets for each setting
|
||||||
|
self.hotkey_input = QtWidgets.QLineEdit()
|
||||||
|
self.discord_presence_check = QtWidgets.QCheckBox()
|
||||||
|
self.auto_update_check = QtWidgets.QCheckBox()
|
||||||
|
self.http_port_spin = QtWidgets.QSpinBox()
|
||||||
|
self.http_port_spin.setRange(1024, 65535)
|
||||||
|
self.dukto_udp_port_spin = QtWidgets.QSpinBox()
|
||||||
|
self.dukto_udp_port_spin.setRange(1024, 65535)
|
||||||
|
self.dukto_tcp_port_spin = QtWidgets.QSpinBox()
|
||||||
|
self.dukto_tcp_port_spin.setRange(1024, 65535)
|
||||||
|
self.search_engine_combo = QtWidgets.QComboBox()
|
||||||
|
self.search_engine_combo.addItems(["brave", "google"])
|
||||||
|
|
||||||
|
# Add widgets to layout
|
||||||
|
self.form_layout.addRow(self.strings.get("hotkey_label", "Global Hotkey:"), self.hotkey_input)
|
||||||
|
self.form_layout.addRow(self.strings.get("discord_presence_label", "Enable Discord Presence:"), self.discord_presence_check)
|
||||||
|
self.form_layout.addRow(self.strings.get("auto_update_label", "Enable Auto-Update:"), self.auto_update_check)
|
||||||
|
self.form_layout.addRow(self.strings.get("http_share_port_label", "HTTP Share Port:"), self.http_port_spin)
|
||||||
|
self.form_layout.addRow(self.strings.get("dukto_udp_port_label", "Dukto UDP Port:"), self.dukto_udp_port_spin)
|
||||||
|
self.form_layout.addRow(self.strings.get("dukto_tcp_port_label", "Dukto TCP Port:"), self.dukto_tcp_port_spin)
|
||||||
|
self.form_layout.addRow(self.strings.get("search_engine_label", "Web Search Engine:"), self.search_engine_combo)
|
||||||
|
|
||||||
|
self.layout.addLayout(self.form_layout) #type: ignore
|
||||||
|
|
||||||
|
# Info label
|
||||||
|
self.info_label = QtWidgets.QLabel(self.strings.get("restart_note", "Note: Some changes may require a restart."))
|
||||||
|
self.info_label.setStyleSheet("font-style: italic; color: grey;")
|
||||||
|
self.info_label.setWordWrap(True)
|
||||||
|
self.layout.addWidget(self.info_label) #type: ignore
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
self.button_box = QtWidgets.QDialogButtonBox(
|
||||||
|
QtWidgets.QDialogButtonBox.Save | QtWidgets.QDialogButtonBox.Cancel | QtWidgets.QDialogButtonBox.Reset # type: ignore
|
||||||
|
)
|
||||||
|
self.button_box.accepted.connect(self.save_config)
|
||||||
|
self.button_box.rejected.connect(self.reject)
|
||||||
|
reset_button = self.button_box.button(QtWidgets.QDialogButtonBox.Reset) # type: ignore
|
||||||
|
if reset_button:
|
||||||
|
reset_button.clicked.connect(self.reset_to_defaults)
|
||||||
|
|
||||||
|
self.layout.addWidget(self.button_box) #type: ignore
|
||||||
|
|
||||||
|
self.load_config()
|
||||||
|
|
||||||
|
def load_config(self):
|
||||||
|
self.hotkey_input.setText(self.config.get("hotkey", ""))
|
||||||
|
self.discord_presence_check.setChecked(self.config.get("discord_presence", True))
|
||||||
|
self.auto_update_check.setChecked(self.config.get("auto_update", True))
|
||||||
|
self.http_port_spin.setValue(self.config.get("http_share_port", 8080))
|
||||||
|
self.dukto_udp_port_spin.setValue(self.config.get("dukto_udp_port", 4644))
|
||||||
|
self.dukto_tcp_port_spin.setValue(self.config.get("dukto_tcp_port", 4644))
|
||||||
|
self.search_engine_combo.setCurrentText(self.config.get("search_engine", "brave"))
|
||||||
|
|
||||||
|
def save_config(self):
|
||||||
|
self.config.set("hotkey", self.hotkey_input.text())
|
||||||
|
self.config.set("discord_presence", self.discord_presence_check.isChecked())
|
||||||
|
self.config.set("auto_update", self.auto_update_check.isChecked())
|
||||||
|
self.config.set("http_share_port", self.http_port_spin.value())
|
||||||
|
self.config.set("dukto_udp_port", self.dukto_udp_port_spin.value())
|
||||||
|
self.config.set("dukto_tcp_port", self.dukto_tcp_port_spin.value())
|
||||||
|
self.config.set("search_engine", self.search_engine_combo.currentText())
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
def reset_to_defaults(self):
|
||||||
|
reply = QtWidgets.QMessageBox.question(
|
||||||
|
self,
|
||||||
|
self.strings.get("reset_title", "Confirm Reset"),
|
||||||
|
self.strings.get("reset_text", "Are you sure?"),
|
||||||
|
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, # type: ignore
|
||||||
|
QtWidgets.QMessageBox.No # type: ignore
|
||||||
|
)
|
||||||
|
if reply == QtWidgets.QMessageBox.Yes: # type: ignore
|
||||||
|
self.config.reset()
|
||||||
|
self.load_config()
|
||||||
@@ -10,12 +10,14 @@ from core.dukto import Peer
|
|||||||
from core.file_search import find
|
from core.file_search import find
|
||||||
from core.web_search import MullvadLetaWrapper
|
from core.web_search import MullvadLetaWrapper
|
||||||
from core.http_share import FileShareServer
|
from core.http_share import FileShareServer
|
||||||
|
from core.config import Config
|
||||||
|
|
||||||
from windows.app_launcher import AppLauncherDialog
|
from windows.app_launcher import AppLauncherDialog
|
||||||
from windows.file_search import FileSearchResults
|
from windows.file_search import FileSearchResults
|
||||||
from windows.web_results import WebSearchResults
|
from windows.web_results import WebSearchResults
|
||||||
from windows.text_viewer import TextViewerDialog
|
from windows.text_viewer import TextViewerDialog
|
||||||
from windows.calculator import CalculatorDialog
|
from windows.calculator import CalculatorDialog
|
||||||
|
from windows.config_window import ConfigWindow
|
||||||
|
|
||||||
ASSET = Path(__file__).parent.parent / "assets" / "2ktan.png"
|
ASSET = Path(__file__).parent.parent / "assets" / "2ktan.png"
|
||||||
|
|
||||||
@@ -38,10 +40,15 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
http_download_signal = QtCore.Signal(str, str)
|
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().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
|
self.config = config
|
||||||
|
self.listener = None
|
||||||
|
|
||||||
|
self.restart = restart
|
||||||
|
self.no_quit = no_quit
|
||||||
|
|
||||||
flags = (
|
flags = (
|
||||||
QtCore.Qt.FramelessWindowHint #type: ignore
|
QtCore.Qt.FramelessWindowHint #type: ignore
|
||||||
@@ -64,12 +71,12 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
mask = QtGui.QBitmap.fromImage(mask_img)
|
mask = QtGui.QBitmap.fromImage(mask_img)
|
||||||
self.setMask(mask)
|
self.setMask(mask)
|
||||||
|
|
||||||
self.super_menu = super_menu
|
|
||||||
self.dukto_handler = dukto_handler
|
self.dukto_handler = dukto_handler
|
||||||
self.progress_dialog = None
|
self.progress_dialog = None
|
||||||
|
|
||||||
# HTTP file sharing
|
# 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)
|
self.http_share.on_download = lambda filename, ip: self.http_download_signal.emit(filename, ip)
|
||||||
|
|
||||||
# Connect Dukto callbacks to emit signals
|
# Connect Dukto callbacks to emit signals
|
||||||
@@ -109,7 +116,8 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
|
|
||||||
# Super key
|
# Super key
|
||||||
self.show_menu_signal.connect(self.show_menu)
|
self.show_menu_signal.connect(self.show_menu)
|
||||||
self.start_hotkey_listener()
|
if config.get("hotkey") != None and config.get("hotkey") != "none" and config.get("hotkey") != "":
|
||||||
|
self.start_hotkey_listener()
|
||||||
|
|
||||||
def build_menus(self):
|
def build_menus(self):
|
||||||
s = self.strings["main_window"]["right_menu"]
|
s = self.strings["main_window"]["right_menu"]
|
||||||
@@ -139,13 +147,14 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
self.share_text_submenu_right = share_menu_right.addMenu(s["share_text_submenu"])
|
self.share_text_submenu_right = share_menu_right.addMenu(s["share_text_submenu"])
|
||||||
self.stop_share_action_right = share_menu_right.addAction("Stop Browser Share", self.stop_browser_share)
|
self.stop_share_action_right = share_menu_right.addAction("Stop Browser Share", self.stop_browser_share)
|
||||||
right_menu.addSeparator()
|
right_menu.addSeparator()
|
||||||
|
right_menu.addAction(s.get("settings", "Settings"), self.start_config_window)
|
||||||
right_menu.addAction(s["check_updates"], self.update_git)
|
right_menu.addAction(s["check_updates"], self.update_git)
|
||||||
|
|
||||||
if "--restart" in sys.argv:
|
if self.restart:
|
||||||
right_menu.addAction(s["restart"], self.restart_application)
|
right_menu.addAction(s["restart"], self.restart_application)
|
||||||
right_menu.addAction(s["toggle_visibility"], self.toggle_visible)
|
right_menu.addAction(s["toggle_visibility"], self.toggle_visible)
|
||||||
right_menu.addSeparator()
|
right_menu.addSeparator()
|
||||||
if "--no-quit" not in sys.argv:
|
if self.no_quit:
|
||||||
right_menu.addAction(s["quit"], QtWidgets.QApplication.quit)
|
right_menu.addAction(s["quit"], QtWidgets.QApplication.quit)
|
||||||
|
|
||||||
self.tray.setContextMenu(right_menu)
|
self.tray.setContextMenu(right_menu)
|
||||||
@@ -200,17 +209,40 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
def show_menu(self):
|
def show_menu(self):
|
||||||
self.left_menu.popup(QtGui.QCursor.pos())
|
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):
|
def start_hotkey_listener(self):
|
||||||
self.listener = keyboard.Listener(on_press=self.on_press)
|
hotkey_str = self.config.get("hotkey", "super")
|
||||||
self.listener.start()
|
|
||||||
|
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):
|
def closeEvent(self, event):
|
||||||
self.listener.stop()
|
if self.listener:
|
||||||
|
self.listener.stop()
|
||||||
if self.http_share.is_running():
|
if self.http_share.is_running():
|
||||||
self.http_share.stop()
|
self.http_share.stop()
|
||||||
super().closeEvent(event)
|
super().closeEvent(event)
|
||||||
@@ -463,6 +495,10 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
self.calculator_dialog.move(QtGui.QCursor.pos())
|
self.calculator_dialog.move(QtGui.QCursor.pos())
|
||||||
self.calculator_dialog.show()
|
self.calculator_dialog.show()
|
||||||
|
|
||||||
|
def start_config_window(self):
|
||||||
|
self.config_dialog = ConfigWindow(self.strings, self.config, self)
|
||||||
|
self.config_dialog.show()
|
||||||
|
|
||||||
def start_file_search(self):
|
def start_file_search(self):
|
||||||
s = self.strings["file_search"]
|
s = self.strings["file_search"]
|
||||||
dialog = QtWidgets.QInputDialog(self)
|
dialog = QtWidgets.QInputDialog(self)
|
||||||
@@ -518,7 +554,8 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
if ok and query:
|
if ok and query:
|
||||||
try:
|
try:
|
||||||
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) #type: ignore
|
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)
|
results = leta.search(query)
|
||||||
|
|
||||||
if results and results.get('results'):
|
if results and results.get('results'):
|
||||||
|
|||||||
Reference in New Issue
Block a user