Compare commits

...

8 Commits

Author SHA1 Message Date
N0\A
d4ff7feb17 Few small different syntex fixes and so on 2025-11-10 11:32:33 +01:00
e38b1b3052 Quick formatting fix should work on GitHub 2025-11-08 10:56:43 +00:00
N0\A
df3cc15356 . 2025-11-08 11:51:27 +01:00
N0\A
0bb4ef7d51 WAYLAND FUCKING WORKS!!! 2025-11-08 11:46:28 +01:00
N0\A
c4b90aaba6 It's now fullscreen. It looks like nothing changed, that's the point 2025-11-08 11:22:35 +01:00
N0\A
aa2c23537c wayland removed 2025-11-07 09:31:14 +01:00
N0\A
69b0fb3226 . 2025-11-07 09:30:27 +01:00
N0\A
1814054cc2 supercopy update 2025-11-07 09:24:13 +01:00
8 changed files with 944 additions and 480 deletions

3
.gitignore vendored
View File

@@ -9,6 +9,7 @@ __pycache__/
*.pyc.*
*.pyo.*
# SUPERCOPY
SUPERCOPY.py
copy.md
.vscode
start.sh

View File

@@ -20,7 +20,6 @@ A WIP desktop assistant for Linux and Windows.
## Requirements
- Python 3
- X11 Desktop (Linux only)
- [`fd`](https://github.com/sharkdp/fd)
- `git`
@@ -33,6 +32,8 @@ A WIP desktop assistant for Linux and Windows.
- `--restart` Add a restart option to the right click menu
- `--no-quit` Hide the quit option
- `--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.
> [!IMPORTANT]
> If on Wayland, make sure to add `QT_QPA_PLATFORM=xcb` at the beginning of the command
If you want to contribute in any way, PRs (and issues) welcome

View File

@@ -1,38 +1,372 @@
files = [
"core/app_launcher.py",
"core/config.py",
"core/discord_presence.py",
"core/dukto.py",
"core/file_search.py",
"core/headers.py",
"core/http_share.py",
"core/updater.py",
"core/wayland_utils.py",
"core/web_search.py",
"strings/en.json",
"strings/personality_en.json",
"windows/app_launcher.py",
"windows/calculator.py",
"windows/file_search.py",
"windows/main_window.py",
"windows/text_viewer.py",
"windows/web_results.py",
"main.py",
"README.md",
"requirements.txt"
import fnmatch
import os
import sys
def get_language(file_path):
extension_map = {
".html": "html",
".htm": "html",
".css": "css",
".js": "javascript",
".mjs": "javascript",
".cjs": "javascript",
".py": "python",
".pyc": "python",
".pyo": "python",
".md": "markdown",
".markdown": "markdown",
".txt": "text",
".json": "json",
".geojson": "json",
".xml": "xml",
".php": "php",
".phtml": "php",
".sql": "sql",
".sh": "bash",
".bash": "bash",
".zsh": "bash",
".fish": "fish",
".yml": "yaml",
".yaml": "yaml",
".toml": "toml",
".ini": "ini",
".cfg": "ini",
".conf": "ini",
".config": "ini",
".log": "text",
".bat": "batch",
".cmd": "batch",
".ps1": "powershell",
".psm1": "powershell",
".psd1": "powershell",
".rb": "ruby",
".gemspec": "ruby",
".go": "go",
".java": "java",
".class": "java",
".c": "c",
".h": "cpp",
".cpp": "cpp",
".cc": "cpp",
".cxx": "cpp",
".c++": "cpp",
".hpp": "cpp",
".hh": "cpp",
".hxx": "cpp",
".cs": "csharp",
".csx": "csharp",
".swift": "swift",
".kt": "kotlin",
".kts": "kotlin",
".rs": "rust",
".ts": "typescript",
".tsx": "typescript",
".mts": "typescript",
".cts": "typescript",
".jsx": "javascript",
".vue": "vue",
".scss": "scss",
".sass": "sass",
".less": "less",
".styl": "stylus",
".stylus": "stylus",
".graphql": "graphql",
".gql": "graphql",
".dockerfile": "dockerfile",
".dockerignore": "dockerignore",
".editorconfig": "ini",
".gitignore": "gitignore",
".gitattributes": "gitattributes",
".gitmodules": "gitmodules",
".prettierrc": "json",
".eslintrc": "json",
".babelrc": "json",
".npmignore": "gitignore",
".lock": "text",
".env": "env",
".env.local": "env",
".env.development": "env",
".env.production": "env",
".env.test": "env",
}
ext = os.path.splitext(file_path)[1].lower()
return extension_map.get(ext, "")
def should_exclude(file_path, root_dir):
"""Determine if a file should be excluded from copying."""
abs_path = os.path.abspath(file_path)
rel_path = os.path.relpath(abs_path, root_dir)
rel_path_forward = rel_path.replace(os.sep, "/")
basename = os.path.basename(file_path)
# Exclude specific files
exclude_files = {".pyc"}
if rel_path_forward in exclude_files or basename in exclude_files:
return True
# Exclude image files
image_extensions = {
".png",
".jpg",
".jpeg",
".gif",
".svg",
".bmp",
".ico",
".tiff",
".tif",
".webp",
".heic",
".heif",
".avif",
".jfif",
".pjpeg",
".pjp",
".tga",
".psd",
".raw",
".cr2",
".nef",
".orf",
".sr2",
".arw",
".dng",
".rw2",
".raf",
".3fr",
".kdc",
".mef",
".mrw",
".pef",
".srw",
".x3f",
".r3d",
".fff",
".iiq",
".erf",
".nrw",
}
ext = os.path.splitext(file_path)[1].lower()
if ext in image_extensions:
return True
return False
def load_gitignore_patterns(root_dir):
patterns = []
gitignore_path = os.path.join(root_dir, ".gitignore")
if os.path.isfile(gitignore_path):
try:
with open(gitignore_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
# Skip empty lines and comments
if line and not line.startswith("#"):
# Remove trailing backslash for escaped #
if line.startswith(r"\#"):
line = line[1:]
patterns.append(line)
except Exception as e:
print(f"Warning: Could not read .gitignore: {e}")
return patterns
def is_ignored(path, patterns, root_dir):
if not patterns:
return False
# Get relative path with forward slashes
rel_path = os.path.relpath(path, root_dir).replace(os.sep, "/")
# For directories, also check with trailing slash
if os.path.isdir(path):
rel_path_with_slash = rel_path + "/"
else:
rel_path_with_slash = rel_path
for pattern in patterns:
# Skip negation patterns (too complex for this script)
if pattern.startswith("!"):
continue
# Directory pattern (ending with /)
if pattern.endswith("/"):
if not os.path.isdir(path):
continue
pattern = pattern.rstrip("/")
# Match directory name or anything inside it
if fnmatch.fnmatch(rel_path, pattern) or fnmatch.fnmatch(
rel_path_with_slash, pattern + "/*"
):
return True
continue
# Absolute pattern (starting with /) - match from root only
if pattern.startswith("/"):
pattern = pattern.lstrip("/")
if fnmatch.fnmatch(rel_path, pattern):
return True
continue
# Pattern without slash - matches at any level
if "/" not in pattern:
# Check basename
basename = os.path.basename(rel_path)
if fnmatch.fnmatch(basename, pattern):
return True
else:
# Pattern with slash - relative path match
if fnmatch.fnmatch(rel_path, pattern):
return True
return False
def get_files_from_directory(
directory, recursive=False, root_dir=None, gitignore_patterns=None
):
if root_dir is None:
root_dir = os.getcwd()
if gitignore_patterns is None:
gitignore_patterns = []
files_list = []
abs_directory = os.path.abspath(directory)
if not os.path.exists(abs_directory):
print(f"Warning: Directory '{directory}' not found.")
return files_list
# Skip if directory itself is ignored
if is_ignored(abs_directory, gitignore_patterns, root_dir):
return files_list
if recursive:
for dirpath, dirnames, filenames in os.walk(abs_directory):
# Filter directories: exclude hidden and gitignored
dirnames[:] = [
d
for d in dirnames
if not d.startswith(".")
and not is_ignored(
os.path.join(dirpath, d), gitignore_patterns, root_dir
)
]
# Filter files
for filename in filenames:
if filename.startswith("."):
continue
full_path = os.path.join(dirpath, filename)
if (
os.path.isfile(full_path)
and not should_exclude(full_path, root_dir)
and not is_ignored(full_path, gitignore_patterns, root_dir)
):
files_list.append(full_path)
else:
for filename in os.listdir(abs_directory):
if filename.startswith("."):
continue
full_path = os.path.join(abs_directory, filename)
# Skip directories in non-recursive mode
if os.path.isdir(full_path):
continue
if (
os.path.isfile(full_path)
and not should_exclude(full_path, root_dir)
and not is_ignored(full_path, gitignore_patterns, root_dir)
):
files_list.append(full_path)
return files_list
def main():
root_dir = os.getcwd()
script_path = os.path.abspath(__file__)
output_file = "copy.md"
codeblock = "```"
copy = ""
# Load .gitignore patterns
gitignore_patterns = load_gitignore_patterns(root_dir)
if gitignore_patterns:
print(f"Loaded {len(gitignore_patterns)} patterns from .gitignore")
for file in files:
with open(file, "r", encoding="utf-8") as f:
lines = f.readlines()
copy += f"### {file}\n\n"
copy += f"{codeblock}python\n"
copy += "".join(lines)
copy += f"\n{codeblock}\n\n"
def is_output_file(path):
return os.path.abspath(path) == os.path.abspath(output_file)
with open("copy.md", "w", encoding="utf-8") as f:
f.write(copy)
# Directories to process: (path, recursive)
directories = [
("./", False), # Root directory
("assets/", True), # Archive directory (with subdirectories)
("core/", True), # Legacy directory
("strings/", True), # Maybe directory
("windows/", True),
]
all_files = []
for directory, recursive in directories:
files = get_files_from_directory(
directory, recursive, root_dir, gitignore_patterns
)
files = [
f
for f in files
if not is_output_file(f) and os.path.abspath(f) != script_path
]
all_files.extend(files)
# Remove duplicates and sort
all_files = sorted(set(all_files))
markdown_content = ""
file_count = 0
for file_path in all_files:
try:
rel_path = os.path.relpath(file_path, root_dir)
language = get_language(file_path)
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
markdown_content += f"### {rel_path.replace(os.sep, '/')}\n\n"
markdown_content += (
f"{codeblock}{language}\n" if language else f"{codeblock}\n"
)
markdown_content += content
markdown_content += f"\n{codeblock}\n\n"
file_count += 1
except UnicodeDecodeError:
print(f"Warning: Could not read {file_path} as text. Skipping.")
except Exception as e:
print(f"Error processing {file_path}: {e}")
markdown_content += f"<!-- Processed {file_count} files -->\n"
try:
with open(output_file, "w", encoding="utf-8") as f:
f.write(markdown_content)
print(f"Successfully created {output_file} with {file_count} files.")
except Exception as e:
print(f"Error writing to {output_file}: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -1,8 +1,8 @@
import fnmatch
import os
import shutil
import subprocess
import os
import platform
import fnmatch
def _find_native(pattern: str, root: str):
"""Native Python implementation of file search using os.walk."""
@@ -12,14 +12,17 @@ def _find_native(pattern: str, root: str):
results.append(os.path.join(dirpath, filename))
return results
def find(pattern: str, root: str='/'):
def find(pattern: str, root: str = "/"):
path = os.path.expanduser(root)
if shutil.which('fd') is None:
if shutil.which("fd") is None:
return _find_native(f"*{pattern}*", path)
else:
try:
out = subprocess.check_output(['fd', pattern, path], text=True, errors='ignore')
out = subprocess.check_output(
["fd", pattern, path], text=True, errors="ignore"
)
return out.splitlines()
except subprocess.CalledProcessError:
return []

View File

@@ -1,107 +0,0 @@
import os
import subprocess
from typing import Tuple, Optional
def is_wayland() -> bool:
session_type = os.environ.get('XDG_SESSION_TYPE', '').lower()
wayland_display = os.environ.get('WAYLAND_DISPLAY', '')
return session_type == 'wayland' or bool(wayland_display)
def get_screen_info() -> Tuple[int, int]:
if is_wayland():
try:
output = subprocess.check_output(['wlr-randr'], text=True, stderr=subprocess.DEVNULL)
for line in output.splitlines():
if 'current' in line.lower():
parts = line.split()
for part in parts:
if 'x' in part and part.replace('x', '').replace('px', '').isdigit():
dims = part.replace('px', '').split('x')
return int(dims[0]), int(dims[1])
except (subprocess.CalledProcessError, FileNotFoundError, PermissionError):
pass
import json
try:
output = subprocess.check_output(['swaymsg', '-t', 'get_outputs'], text=True)
outputs = json.loads(output)
if outputs and outputs[0].get('current_mode'):
mode = outputs[0]['current_mode']
return mode['width'], mode['height']
except (subprocess.CalledProcessError, FileNotFoundError, json.JSONDecodeError, KeyError, PermissionError):
pass
try:
output = subprocess.check_output(['kscreen-doctor', '-o'], text=True)
for line in output.splitlines():
if 'Output:' in line:
continue
if 'x' in line and '@' in line:
resolution = line.split('@')[0].strip().split()[-1]
if 'x' in resolution:
dims = resolution.split('x')
return int(dims[0]), int(dims[1])
except (subprocess.CalledProcessError, FileNotFoundError, PermissionError):
pass
try:
output = subprocess.check_output(['xrandr'], text=True, stderr=subprocess.DEVNULL)
for line in output.splitlines():
if ' connected' in line and 'primary' in line:
parts = line.split()
for part in parts:
if 'x' in part and '+' in part:
dims = part.split('+')[0].split('x')
return int(dims[0]), int(dims[1])
elif ' connected' in line and '*' in line:
parts = line.split()
for i, part in enumerate(parts):
if 'x' in part and i > 0:
dims = part.split('x')
if dims[0].isdigit():
return int(dims[0]), int(dims[1].split('+')[0] if '+' in dims[1] else dims[1])
except (subprocess.CalledProcessError, FileNotFoundError, PermissionError):
pass
# maybe somehow right sometimes
return 1920, 1080
def set_window_bottom_right_wayland(window, width: int, height: int):
screen_w, screen_h = get_screen_info()
x = screen_w - width
y = screen_h - height
try:
window.move(x, y)
except:
pass
def get_wayland_compositor() -> Optional[str]:
desktop = os.environ.get('XDG_CURRENT_DESKTOP', '').lower()
if 'sway' in desktop:
return 'sway'
elif 'kde' in desktop or 'plasma' in desktop:
return 'kwin'
elif 'gnome' in desktop:
return 'mutter'
elif 'hypr' in desktop:
return 'hyprland'
# detect from process list
try:
output = subprocess.check_output(['ps', 'aux'], text=True)
if 'sway' in output:
return 'sway'
elif 'kwin_wayland' in output:
return 'kwin'
elif 'gnome-shell' in output:
return 'mutter'
elif 'Hyprland' in output:
return 'hyprland'
except:
pass
return None

View File

@@ -1,7 +1,10 @@
# TODO: Switch to s different search provider
from typing import Any, Dict, Optional
import requests
from typing import Optional, Dict, List, Any
from urllib.parse import urlencode
from bs4 import BeautifulSoup
from core.headers import get_useragent
@@ -15,18 +18,79 @@ class MullvadLetaWrapper:
# Available countries (from the HTML)
COUNTRIES = [
"ar", "au", "at", "be", "br", "ca", "cl", "cn", "dk", "fi",
"fr", "de", "hk", "in", "id", "it", "jp", "kr", "my", "mx",
"nl", "nz", "no", "ph", "pl", "pt", "ru", "sa", "za", "es",
"se", "ch", "tw", "tr", "uk", "us"
"ar",
"au",
"at",
"be",
"br",
"ca",
"cl",
"cn",
"dk",
"fi",
"fr",
"de",
"hk",
"in",
"id",
"it",
"jp",
"kr",
"my",
"mx",
"nl",
"nz",
"no",
"ph",
"pl",
"pt",
"ru",
"sa",
"za",
"es",
"se",
"ch",
"tw",
"tr",
"uk",
"us",
]
# Available languages
LANGUAGES = [
"ar", "bg", "ca", "zh-hans", "zh-hant", "hr", "cs", "da", "nl",
"en", "et", "fi", "fr", "de", "he", "hu", "is", "it", "jp",
"ko", "lv", "lt", "nb", "pl", "pt", "ro", "ru", "sr", "sk",
"sl", "es", "sv", "tr"
"ar",
"bg",
"ca",
"zh-hans",
"zh-hant",
"hr",
"cs",
"da",
"nl",
"en",
"et",
"fi",
"fr",
"de",
"he",
"hu",
"is",
"it",
"jp",
"ko",
"lv",
"lt",
"nb",
"pl",
"pt",
"ro",
"ru",
"sr",
"sk",
"sl",
"es",
"sv",
"tr",
]
# Time filters
@@ -59,7 +123,7 @@ class MullvadLetaWrapper:
"sec-fetch-site": "same-origin",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1",
"user-agent": get_useragent()
"user-agent": get_useragent(),
}
def search(
@@ -68,7 +132,7 @@ class MullvadLetaWrapper:
country: Optional[str] = None,
language: Optional[str] = None,
last_updated: Optional[str] = None,
page: int = 1
page: int = 1,
) -> Dict[str, Any]:
"""
Perform a search on Mullvad Leta.
@@ -93,10 +157,7 @@ class MullvadLetaWrapper:
raise ValueError(f"Invalid time filter. Must be one of {self.TIME_FILTERS}")
# Build query parameters
params = {
"q": query,
"engine": self.engine
}
params = {"q": query, "engine": self.engine}
if country:
params["country"] = country
@@ -116,7 +177,7 @@ class MullvadLetaWrapper:
params=params,
headers=self._get_headers(),
cookies=cookies,
timeout=10
timeout=10,
)
response.raise_for_status()
@@ -135,7 +196,7 @@ class MullvadLetaWrapper:
Returns:
Dictionary containing parsed results
"""
soup = BeautifulSoup(html, 'html.parser')
soup = BeautifulSoup(html, "html.parser")
results = {
"query": query,
@@ -144,37 +205,37 @@ class MullvadLetaWrapper:
"results": [],
"infobox": None,
"news": [],
"cached": False
"cached": False,
}
# Check if cached
cache_notice = soup.find('p', class_='small')
if cache_notice and 'cached' in cache_notice.text.lower():
cache_notice = soup.find("p", class_="small")
if cache_notice and "cached" in cache_notice.text.lower():
results["cached"] = True
# Extract regular search results
articles = soup.find_all('article', class_='svelte-fmlk7p')
articles = soup.find_all("article", class_="svelte-fmlk7p")
for article in articles:
result = self._parse_article(article)
if result:
results["results"].append(result)
# Extract infobox if present
infobox_div = soup.find('div', class_='infobox')
infobox_div = soup.find("div", class_="infobox")
if infobox_div:
results["infobox"] = self._parse_infobox(infobox_div)
# Extract news results
news_div = soup.find('div', class_='news')
news_div = soup.find("div", class_="news")
if news_div:
news_articles = news_div.find_all('article')
news_articles = news_div.find_all("article")
for article in news_articles:
news_item = self._parse_news_article(article)
if news_item:
results["news"].append(news_item)
# Check for next page
next_button = soup.find('button', {'data-cy': 'next-button'})
next_button = soup.find("button", {"data-cy": "next-button"})
results["has_next_page"] = next_button is not None
return results
@@ -182,19 +243,19 @@ class MullvadLetaWrapper:
def _parse_article(self, article) -> Optional[Dict[str, str]]:
"""Parse a single search result article."""
try:
link_tag = article.find('a', href=True)
link_tag = article.find("a", href=True)
if not link_tag:
return None
title_tag = article.find('h3')
snippet_tag = article.find('p', class_='result__body')
cite_tag = article.find('cite')
title_tag = article.find("h3")
snippet_tag = article.find("p", class_="result__body")
cite_tag = article.find("cite")
return {
"url": link_tag['href'],
"url": link_tag["href"],
"title": title_tag.get_text(strip=True) if title_tag else "",
"snippet": snippet_tag.get_text(strip=True) if snippet_tag else "",
"display_url": cite_tag.get_text(strip=True) if cite_tag else ""
"display_url": cite_tag.get_text(strip=True) if cite_tag else "",
}
except Exception as e:
print(f"Error parsing article: {e}")
@@ -204,19 +265,19 @@ class MullvadLetaWrapper:
"""Parse infobox information."""
infobox = {}
title_tag = infobox_div.find('h1')
title_tag = infobox_div.find("h1")
if title_tag:
infobox["title"] = title_tag.get_text(strip=True)
subtitle_tag = infobox_div.find('h2')
subtitle_tag = infobox_div.find("h2")
if subtitle_tag:
infobox["subtitle"] = subtitle_tag.get_text(strip=True)
url_tag = infobox_div.find('a', rel='noreferrer')
url_tag = infobox_div.find("a", rel="noreferrer")
if url_tag:
infobox["url"] = url_tag['href']
infobox["url"] = url_tag["href"]
desc_tag = infobox_div.find('p')
desc_tag = infobox_div.find("p")
if desc_tag:
infobox["description"] = desc_tag.get_text(strip=True)
@@ -225,19 +286,21 @@ class MullvadLetaWrapper:
def _parse_news_article(self, article) -> Optional[Dict[str, str]]:
"""Parse a news article."""
try:
link_tag = article.find('a', href=True)
link_tag = article.find("a", href=True)
if not link_tag:
return None
title_tag = link_tag.find('h3')
cite_tag = link_tag.find('cite')
time_tag = link_tag.find('time')
title_tag = link_tag.find("h3")
cite_tag = link_tag.find("cite")
time_tag = link_tag.find("time")
return {
"url": link_tag['href'],
"url": link_tag["href"],
"title": title_tag.get_text(strip=True) if title_tag else "",
"source": cite_tag.get_text(strip=True) if cite_tag else "",
"timestamp": time_tag['datetime'] if time_tag and time_tag.has_attr('datetime') else ""
"timestamp": time_tag["datetime"]
if time_tag and time_tag.has_attr("datetime")
else "",
}
except Exception as e:
print(f"Error parsing news article: {e}")
@@ -258,13 +321,13 @@ if __name__ == "__main__":
print(f"Cached: {results['cached']}")
print(f"\nFound {len(results['results'])} results:\n")
for i, result in enumerate(results['results'][:5], 1):
for i, result in enumerate(results["results"][:5], 1):
print(f"{i}. {result['title']}")
print(f" URL: {result['url']}")
print(f" {result['snippet'][:100]}...\n")
if results['news']:
if results["news"]:
print(f"\nNews ({len(results['news'])} items):")
for news in results['news'][:3]:
for news in results["news"][:3]:
print(f"- {news['title']}")
print(f" {news['source']}\n")

27
main.py
View File

@@ -1,35 +1,40 @@
#!/usr/bin/python3
import sys, json
from pathlib import Path
from PySide6 import QtWidgets
import json
import sys
import threading
from pathlib import Path
from PySide6 import QtWidgets
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 core.discord_presence import presence
from core.dukto import DuktoProtocol
from core.updater import is_update_available, update_repository
from windows.main_window import MainWindow
STRINGS_PATH = Path(__file__).parent / "strings" / "personality_en.json"
def preload_apps():
print("Preloading application list...")
list_apps()
print("Application list preloaded.")
def main():
app = QtWidgets.QApplication(sys.argv)
try:
with open(STRINGS_PATH, 'r', encoding='utf-8') as f:
with open(STRINGS_PATH, "r", encoding="utf-8") as f:
strings = json.load(f)
except (FileNotFoundError, json.JSONDecodeError) as e:
print(f"Error loading strings file: {e}")
error_dialog = QtWidgets.QMessageBox()
error_dialog.setIcon(QtWidgets.QMessageBox.Critical) # type: ignore
error_dialog.setText(f"Could not load required strings file from:\n{STRINGS_PATH}")
error_dialog.setText(
f"Could not load required strings file from:\n{STRINGS_PATH}"
)
error_dialog.setWindowTitle("Fatal Error")
error_dialog.exec()
sys.exit(1)
@@ -52,7 +57,7 @@ def main():
dukto_handler = DuktoProtocol()
dukto_handler.set_ports(
udp_port=config.get("dukto_udp_port", 4644),
tcp_port=config.get("dukto_tcp_port", 4644)
tcp_port=config.get("dukto_tcp_port", 4644),
)
pet = MainWindow(
@@ -69,8 +74,6 @@ def main():
dukto_handler.initialize()
dukto_handler.say_hello()
pet.position_bottom_right()
pet.show()
app.aboutToQuit.connect(presence.end)

View File

@@ -1,27 +1,27 @@
from PySide6 import QtCore, QtGui, QtWidgets
from pathlib import Path
import subprocess
from pynput import keyboard
import sys
from pathlib import Path
from core.updater import update_repository, is_update_available
from pynput import keyboard
from PySide6 import QtCore, QtGui, QtWidgets
from core.config import Config
from core.discord_presence import presence
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 core.wayland_utils import is_wayland, get_screen_info
from core.updater import is_update_available, update_repository
from core.web_search import MullvadLetaWrapper
from windows.app_launcher import AppLauncherDialog
from windows.file_search import FileSearchResults
from windows.web_results import WebSearchResults
from windows.text_viewer import TextViewerDialog
from windows.calculator import CalculatorDialog
from windows.config_window import ConfigWindow
from windows.file_search import FileSearchResults
from windows.text_viewer import TextViewerDialog
from windows.web_results import WebSearchResults
ASSET = Path(__file__).parent.parent / "assets" / "2ktan.png"
class MainWindow(QtWidgets.QMainWindow):
show_menu_signal = QtCore.Signal()
@@ -40,28 +40,18 @@ class MainWindow(QtWidgets.QMainWindow):
# HTTP share signals
http_download_signal = QtCore.Signal(str, str)
def __init__(self, dukto_handler, strings, config: Config, restart=False, no_quit=False):
def __init__(
self, dukto_handler, strings, config: Config, restart=False, no_quit=False
):
super().__init__()
self.strings = strings
self.config = config
self.listener = None
self.is_wayland = is_wayland()
self.restart = restart
self.no_quit = no_quit
# Configure window flags based on display server
if self.is_wayland:
# Wayland-compatible flags
flags = (
QtCore.Qt.FramelessWindowHint #type: ignore
| QtCore.Qt.WindowStaysOnTopHint #type: ignore
| QtCore.Qt.Tool #type: ignore
)
else:
# X11 flags
flags = (
QtCore.Qt.FramelessWindowHint # type: ignore
| QtCore.Qt.WindowStaysOnTopHint # type: ignore
@@ -72,21 +62,17 @@ class MainWindow(QtWidgets.QMainWindow):
self.setWindowFlags(flags)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground) # type: ignore
# On Wayland, set additional window properties to prevent Alt+Tab
if self.is_wayland:
self.setAttribute(QtCore.Qt.WA_X11NetWmWindowTypeUtility) #type: ignore
# Load the image
pix = QtGui.QPixmap(str(ASSET))
self.image_size = pix.size()
# Create a label for the image
self.label = QtWidgets.QLabel(self)
self.label.setPixmap(pix)
self.label.resize(pix.size())
self.resize(pix.size())
img = pix.toImage()
mask_img = img.createAlphaMask()
mask = QtGui.QBitmap.fromImage(mask_img)
self.setMask(mask)
# Install event filter on the image to handle clicks
self.label.installEventFilter(self)
self.dukto_handler = dukto_handler
self.progress_dialog = None
@@ -94,18 +80,36 @@ class MainWindow(QtWidgets.QMainWindow):
# HTTP file sharing
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
self.dukto_handler.on_peer_added = lambda peer: self.peer_added_signal.emit(peer)
self.dukto_handler.on_peer_removed = lambda peer: self.peer_removed_signal.emit(peer)
self.dukto_handler.on_receive_request = lambda ip: self.receive_request_signal.emit(ip)
self.dukto_handler.on_transfer_progress = lambda total, rec: self.progress_update_signal.emit(total, rec)
self.dukto_handler.on_receive_start = lambda ip: self.receive_start_signal.emit(ip)
self.dukto_handler.on_receive_complete = lambda files, size: self.receive_complete_signal.emit(files, size)
self.dukto_handler.on_receive_text = lambda text, size: self.receive_text_signal.emit(text, size)
self.dukto_handler.on_peer_added = lambda peer: self.peer_added_signal.emit(
peer
)
self.dukto_handler.on_peer_removed = lambda peer: self.peer_removed_signal.emit(
peer
)
self.dukto_handler.on_receive_request = (
lambda ip: self.receive_request_signal.emit(ip)
)
self.dukto_handler.on_transfer_progress = (
lambda total, rec: self.progress_update_signal.emit(total, rec)
)
self.dukto_handler.on_receive_start = lambda ip: self.receive_start_signal.emit(
ip
)
self.dukto_handler.on_receive_complete = (
lambda files, size: self.receive_complete_signal.emit(files, size)
)
self.dukto_handler.on_receive_text = (
lambda text, size: self.receive_text_signal.emit(text, size)
)
self.dukto_handler.on_send_start = lambda ip: self.send_start_signal.emit(ip)
self.dukto_handler.on_send_complete = lambda files: self.send_complete_signal.emit(files)
self.dukto_handler.on_send_complete = (
lambda files: self.send_complete_signal.emit(files)
)
self.dukto_handler.on_error = lambda msg: self.dukto_error_signal.emit(msg)
# Connect signals to GUI slots
@@ -133,24 +137,47 @@ class MainWindow(QtWidgets.QMainWindow):
# Super key
self.show_menu_signal.connect(self.show_menu)
if config.get("hotkey") != None and config.get("hotkey") != "none" and config.get("hotkey") != "":
if (
config.get("hotkey") is not None
and config.get("hotkey") != "none"
and config.get("hotkey") != ""
):
self.start_hotkey_listener()
def position_bottom_right(self):
"""Position window at bottom right corner."""
if self.is_wayland:
# On Wayland, get screen info and position window
screen_w, screen_h = get_screen_info()
x = screen_w - self.width()
y = screen_h - self.height()
self.move(x, y)
else:
# On X11, use Qt's screen geometry
screen_geometry = QtWidgets.QApplication.primaryScreen().availableGeometry()
pet_geometry = self.frameGeometry()
x = screen_geometry.width() - pet_geometry.width()
y = screen_geometry.height() - pet_geometry.height()
self.move(x, y)
def showEvent(self, event):
super().showEvent(event)
screen = QtWidgets.QApplication.primaryScreen()
screen_geometry = screen.availableGeometry()
self.setGeometry(screen_geometry)
# Position the image bottom right
label_x = screen_geometry.width() - self.image_size.width()
label_y = screen_geometry.height() - self.image_size.height()
self.label.move(label_x, label_y)
# Create a mask for the window based on the image position
self.update_mask()
self.raise_()
def update_mask(self):
# Create a mask that only includes the image area
mask = QtGui.QRegion(0, 0, 0, 0) # Empty (duh, it says 0)
# Add the image region
image_rect = self.label.geometry()
pixmap = self.label.pixmap()
if pixmap and not pixmap.isNull():
img = pixmap.toImage()
alpha_mask = img.createAlphaMask()
bitmap_mask = QtGui.QBitmap.fromImage(alpha_mask)
image_region = QtGui.QRegion(bitmap_mask)
image_region.translate(image_rect.x(), image_rect.y())
mask = mask.united(image_region)
self.setMask(mask)
def build_menus(self):
s = self.strings["main_window"]["right_menu"]
@@ -160,12 +187,18 @@ class MainWindow(QtWidgets.QMainWindow):
self.left_menu.addAction(s["launch_app"], self.start_app_launcher)
self.left_menu.addAction(s["search_files"], self.start_file_search)
self.left_menu.addAction(s["search_web"], self.start_web_search)
self.left_menu.addAction(s.get("calculator", "Calculator"), self.start_calculator)
self.left_menu.addAction(
s.get("calculator", "Calculator"), self.start_calculator
)
self.left_menu.addSeparator()
share_menu_left = self.left_menu.addMenu(s["share_menu"])
self.share_files_submenu_left = share_menu_left.addMenu(s["share_files_submenu"])
self.share_files_submenu_left = share_menu_left.addMenu(
s["share_files_submenu"]
)
self.share_text_submenu_left = share_menu_left.addMenu(s["share_text_submenu"])
self.stop_share_action_left = share_menu_left.addAction("Stop Browser Share", self.stop_browser_share)
self.stop_share_action_left = share_menu_left.addAction(
"Stop Browser Share", self.stop_browser_share
)
self.left_menu.addSeparator()
# RIGHT MENU (Tray icon)
@@ -176,9 +209,15 @@ class MainWindow(QtWidgets.QMainWindow):
right_menu.addAction(s.get("calculator", "Calculator"), self.start_calculator)
right_menu.addSeparator()
share_menu_right = right_menu.addMenu(s["share_menu"])
self.share_files_submenu_right = share_menu_right.addMenu(s["share_files_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.share_files_submenu_right = share_menu_right.addMenu(
s["share_files_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
)
right_menu.addSeparator()
right_menu.addAction(s.get("settings", "Settings"), self.start_config_window)
right_menu.addAction(s["check_updates"], self.update_git)
@@ -211,34 +250,43 @@ class MainWindow(QtWidgets.QMainWindow):
# Configure file share menus
for menu in [self.share_files_submenu_left, self.share_files_submenu_right]:
for action in menu.actions():
if hasattr(action, 'is_browser_action'):
if hasattr(action, "is_browser_action"):
menu.removeAction(action)
action_text = "Add File(s)..." if has_shared_files else s_menu["via_browser"]
action_text = (
"Add File(s)..." if has_shared_files else s_menu["via_browser"]
)
browser_action = menu.addAction(action_text)
browser_action.is_browser_action = True
browser_action.triggered.connect(self.start_file_share_browser)
if any(not a.isSeparator() and not hasattr(a, 'is_browser_action') for a in menu.actions()):
if any(
not a.isSeparator() and not hasattr(a, "is_browser_action")
for a in menu.actions()
):
if not any(a.isSeparator() for a in menu.actions()):
menu.addSeparator()
# Configure text share menus
for menu in [self.share_text_submenu_left, self.share_text_submenu_right]:
for action in menu.actions():
if hasattr(action, 'is_browser_action'):
if hasattr(action, "is_browser_action"):
menu.removeAction(action)
action_text = "Change Shared Text..." if has_shared_text else s_menu["via_browser"]
action_text = (
"Change Shared Text..." if has_shared_text else s_menu["via_browser"]
)
browser_action = menu.addAction(action_text)
browser_action.is_browser_action = True
browser_action.triggered.connect(self.start_text_share_browser)
if any(not a.isSeparator() and not hasattr(a, 'is_browser_action') for a in menu.actions()):
if any(
not a.isSeparator() and not hasattr(a, "is_browser_action")
for a in menu.actions()
):
if not any(a.isSeparator() for a in menu.actions()):
menu.addSeparator()
def show_menu(self):
self.left_menu.popup(QtGui.QCursor.pos())
@@ -256,17 +304,19 @@ class MainWindow(QtWidgets.QMainWindow):
}
try:
keys = [key_map.get(k.strip().lower(), k.strip().lower()) for k in hotkey_str.split('+')]
formatted_hotkey = '<' + '>+<'.join(keys) + '>' #type: ignore
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
keyboard.HotKey.parse(formatted_hotkey), on_activate
)
self.listener = keyboard.Listener(
on_press=hotkey.press, # type: ignore
on_release=hotkey.release #type: ignore
on_release=hotkey.release, # type: ignore
)
self.listener.start()
@@ -282,23 +332,27 @@ class MainWindow(QtWidgets.QMainWindow):
super().closeEvent(event)
def ensure_on_top(self):
if self.isVisible() and not self.left_menu.isVisible() and not self.tray.contextMenu().isVisible():
if (
self.isVisible()
and not self.left_menu.isVisible()
and not self.tray.contextMenu().isVisible()
):
self.raise_()
# Re-apply window flags to ensure staying on top on Wayland
if self.is_wayland:
self.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint, True) #type: ignore
def showEvent(self, event):
super().showEvent(event)
self.raise_()
# Ensure bottom right positioning
QtCore.QTimer.singleShot(100, self.position_bottom_right)
def mousePressEvent(self, event: QtGui.QMouseEvent):
def eventFilter(self, obj, event): # type: ignore
# Handle mouse events on the image
if obj == self.label:
if event.type() == QtCore.QEvent.MouseButtonPress: # type: ignore
if event.button() == QtCore.Qt.LeftButton: # type: ignore
self.left_menu.popup(event.globalPosition().toPoint())
return True
elif event.button() == QtCore.Qt.RightButton: # type: ignore
self.tray.contextMenu().popup(event.globalPosition().toPoint())
return True
return super().eventFilter(obj, event)
def mousePressEvent(self, event: QtGui.QMouseEvent):
pass
def handle_tray_activated(self, reason):
if reason == QtWidgets.QSystemTrayIcon.ActivationReason.Trigger:
@@ -313,35 +367,64 @@ class MainWindow(QtWidgets.QMainWindow):
peers = list(self.dukto_handler.peers.values())
for menu in [self.share_files_submenu_left, self.share_files_submenu_right, self.share_text_submenu_left, self.share_text_submenu_right]:
actions_to_remove = [a for a in menu.actions() if not a.isSeparator() and not hasattr(a, 'is_browser_action')]
for menu in [
self.share_files_submenu_left,
self.share_files_submenu_right,
self.share_text_submenu_left,
self.share_text_submenu_right,
]:
actions_to_remove = [
a
for a in menu.actions()
if not a.isSeparator() and not hasattr(a, "is_browser_action")
]
for action in actions_to_remove:
menu.removeAction(action)
if not peers:
for menu in [self.share_files_submenu_left, self.share_files_submenu_right, self.share_text_submenu_left, self.share_text_submenu_right]:
for menu in [
self.share_files_submenu_left,
self.share_files_submenu_right,
self.share_text_submenu_left,
self.share_text_submenu_right,
]:
action = menu.addAction(no_peers_str)
action.setEnabled(False)
else:
for peer in sorted(peers, key=lambda p: p.signature):
for files_menu, text_menu in [(self.share_files_submenu_left, self.share_text_submenu_left), (self.share_files_submenu_right, self.share_text_submenu_right)]:
for files_menu, text_menu in [
(self.share_files_submenu_left, self.share_text_submenu_left),
(self.share_files_submenu_right, self.share_text_submenu_right),
]:
file_action = files_menu.addAction(peer.signature)
text_action = text_menu.addAction(peer.signature)
file_action.triggered.connect(lambda checked=False, p=peer: self.start_file_send(p))
text_action.triggered.connect(lambda checked=False, p=peer: self.start_text_send(p))
file_action.triggered.connect(
lambda checked=False, p=peer: self.start_file_send(p)
)
text_action.triggered.connect(
lambda checked=False, p=peer: self.start_text_send(p)
)
self.update_share_menu_state()
def start_file_send(self, peer: Peer):
dialog_title = self.strings["main_window"]["send_files_dialog_title"].format(peer_signature=peer.signature)
file_paths, _ = QtWidgets.QFileDialog.getOpenFileNames(self, dialog_title, str(Path.home()))
dialog_title = self.strings["main_window"]["send_files_dialog_title"].format(
peer_signature=peer.signature
)
file_paths, _ = QtWidgets.QFileDialog.getOpenFileNames(
self, dialog_title, str(Path.home())
)
if file_paths:
self.dukto_handler.send_file(peer.address, file_paths, peer.port)
def start_text_send(self, peer: Peer):
dialog_title = self.strings["main_window"]["send_text_dialog_title"].format(peer_signature=peer.signature)
dialog_title = self.strings["main_window"]["send_text_dialog_title"].format(
peer_signature=peer.signature
)
dialog_label = self.strings["main_window"]["send_text_dialog_label"]
text, ok = QtWidgets.QInputDialog.getMultiLineText(self, dialog_title, dialog_label)
text, ok = QtWidgets.QInputDialog.getMultiLineText(
self, dialog_title, dialog_label
)
if ok and text:
self.dukto_handler.send_text(peer.address, text, peer.port)
@@ -349,8 +432,12 @@ class MainWindow(QtWidgets.QMainWindow):
s = self.strings["main_window"]
is_adding = bool(self.http_share.shared_files)
dialog_title = "Select files to add" if is_adding else s["share_browser_dialog_title"]
file_paths, _ = QtWidgets.QFileDialog.getOpenFileNames(self, dialog_title, str(Path.home()))
dialog_title = (
"Select files to add" if is_adding else s["share_browser_dialog_title"]
)
file_paths, _ = QtWidgets.QFileDialog.getOpenFileNames(
self, dialog_title, str(Path.home())
)
if not file_paths:
return
@@ -358,7 +445,12 @@ class MainWindow(QtWidgets.QMainWindow):
try:
if is_adding:
self.http_share.add_files(file_paths)
self.tray.showMessage("Files Added", f"{len(file_paths)} file(s) added to the share.", QtWidgets.QSystemTrayIcon.Information, 2000) #type: ignore
self.tray.showMessage(
"Files Added",
f"{len(file_paths)} file(s) added to the share.",
QtWidgets.QSystemTrayIcon.Information, # type:ignore
2000,
) # type: ignore
else:
url = self.http_share.share_files(file_paths)
main_text = s["share_browser_text_files"]
@@ -369,7 +461,9 @@ class MainWindow(QtWidgets.QMainWindow):
self.update_share_menu_state()
except Exception as e:
QtWidgets.QMessageBox.critical(self, s["share_error_title"], s["share_error_text"].format(error=str(e)))
QtWidgets.QMessageBox.critical(
self, s["share_error_title"], s["share_error_text"].format(error=str(e))
)
def start_text_share_browser(self):
s = self.strings["main_window"]
@@ -379,7 +473,7 @@ class MainWindow(QtWidgets.QMainWindow):
self,
s["share_text_browser_dialog_title"],
s["share_text_browser_dialog_label"],
self.http_share.shared_text or ""
self.http_share.shared_text or "",
)
if not (ok and text):
@@ -395,13 +489,20 @@ class MainWindow(QtWidgets.QMainWindow):
self.update_share_menu_state()
except Exception as e:
QtWidgets.QMessageBox.critical(self, s["share_error_title"], s["share_error_text"].format(error=str(e)))
QtWidgets.QMessageBox.critical(
self, s["share_error_title"], s["share_error_text"].format(error=str(e))
)
def stop_browser_share(self):
s = self.strings["main_window"]
if self.http_share.is_running():
self.http_share.stop()
self.tray.showMessage(s["sharing_stopped_title"], s["sharing_stopped_text"], QtWidgets.QSystemTrayIcon.Information, 2000) #type: ignore
self.tray.showMessage(
s["sharing_stopped_title"],
s["sharing_stopped_text"],
QtWidgets.QSystemTrayIcon.Information, # type:ignore
2000,
) # type: ignore
self.update_share_menu_state()
def _show_sharing_dialog(self, url: str, main_text: str, info_text: str):
@@ -422,7 +523,12 @@ class MainWindow(QtWidgets.QMainWindow):
if clicked == copy_btn:
clipboard = QtWidgets.QApplication.clipboard()
clipboard.setText(url)
self.tray.showMessage(s["url_copied_title"], s["url_copied_text"], QtWidgets.QSystemTrayIcon.Information, 2000) #type: ignore
self.tray.showMessage(
s["url_copied_title"],
s["url_copied_text"],
QtWidgets.QSystemTrayIcon.Information, # type: ignore
2000,
) # type: ignore
elif clicked == open_btn:
QtGui.QDesktopServices.openUrl(QtCore.QUrl(url))
@@ -433,16 +539,19 @@ class MainWindow(QtWidgets.QMainWindow):
s["download_notification_title"],
s["download_notification_text"].format(filename=filename, ip=client_ip),
QtWidgets.QSystemTrayIcon.Information, # type: ignore
3000
3000,
)
def show_receive_confirmation(self, sender_ip: str):
reply = QtWidgets.QMessageBox.question(
self,
self.strings["main_window"]["receive_confirm_title"],
self.strings["main_window"]["receive_confirm_text"].format(sender_ip=sender_ip),
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
QtWidgets.QMessageBox.StandardButton.No
self.strings["main_window"]["receive_confirm_text"].format(
sender_ip=sender_ip
),
QtWidgets.QMessageBox.StandardButton.Yes
| QtWidgets.QMessageBox.StandardButton.No,
QtWidgets.QMessageBox.StandardButton.No,
)
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
self.dukto_handler.approve_transfer()
@@ -452,15 +561,21 @@ class MainWindow(QtWidgets.QMainWindow):
@QtCore.Slot(str)
def handle_receive_start(self, sender_ip: str):
s = self.strings["main_window"]["progress_dialog"]
self.progress_dialog = QtWidgets.QProgressDialog(s["receiving_label"], s["cancel_button"], 0, 100, self)
self.progress_dialog.setWindowTitle(s["receiving_title"].format(sender_ip=sender_ip))
self.progress_dialog = QtWidgets.QProgressDialog(
s["receiving_label"], s["cancel_button"], 0, 100, self
)
self.progress_dialog.setWindowTitle(
s["receiving_title"].format(sender_ip=sender_ip)
)
self.progress_dialog.setWindowModality(QtCore.Qt.WindowModal) # type: ignore
self.progress_dialog.show()
@QtCore.Slot(str)
def handle_send_start(self, dest_ip: str):
s = self.strings["main_window"]["progress_dialog"]
self.progress_dialog = QtWidgets.QProgressDialog(s["sending_label"], s["cancel_button"], 0, 100, self)
self.progress_dialog = QtWidgets.QProgressDialog(
s["sending_label"], s["cancel_button"], 0, 100, self
)
self.progress_dialog.setWindowTitle(s["sending_title"].format(dest_ip=dest_ip))
self.progress_dialog.setWindowModality(QtCore.Qt.WindowModal) # type: ignore
self.progress_dialog.show()
@@ -479,14 +594,19 @@ class MainWindow(QtWidgets.QMainWindow):
self.progress_dialog = None
s = self.strings["main_window"]
QtWidgets.QMessageBox.information(self, s["receive_complete_title"], s["receive_complete_text"].format(count=len(received_files)))
QtWidgets.QMessageBox.information(
self,
s["receive_complete_title"],
s["receive_complete_text"].format(count=len(received_files)),
)
reply = QtWidgets.QMessageBox.question(
self,
s["open_folder_title"],
s["open_folder_text"],
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
QtWidgets.QMessageBox.StandardButton.Yes
| QtWidgets.QMessageBox.StandardButton.No,
QtWidgets.QMessageBox.StandardButton.Yes,
)
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
@@ -504,9 +624,15 @@ class MainWindow(QtWidgets.QMainWindow):
s = self.strings["main_window"]
if sent_files and sent_files[0] == "___DUKTO___TEXT___":
QtWidgets.QMessageBox.information(self, s["send_complete_title"], s["send_complete_text_single"])
QtWidgets.QMessageBox.information(
self, s["send_complete_title"], s["send_complete_text_single"]
)
else:
QtWidgets.QMessageBox.information(self, s["send_complete_title"], s["send_complete_text"].format(count=len(sent_files)))
QtWidgets.QMessageBox.information(
self,
s["send_complete_title"],
s["send_complete_text"].format(count=len(sent_files)),
)
@QtCore.Slot(str, int)
def handle_receive_text(self, text: str, total_size: int):
@@ -522,7 +648,11 @@ class MainWindow(QtWidgets.QMainWindow):
if self.progress_dialog:
self.progress_dialog.close()
self.progress_dialog = None
QtWidgets.QMessageBox.critical(self, self.strings["main_window"]["dukto_error_title"], self.strings["main_window"]["dukto_error_text"].format(error_msg=error_msg))
QtWidgets.QMessageBox.critical(
self,
self.strings["main_window"]["dukto_error_title"],
self.strings["main_window"]["dukto_error_text"].format(error_msg=error_msg),
)
def start_app_launcher(self):
self.app_launcher_dialog = AppLauncherDialog(self.strings, self)
@@ -551,9 +681,11 @@ class MainWindow(QtWidgets.QMainWindow):
if ok and pattern:
try:
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) # type: ignore
results = find(pattern, root='~')
results = find(pattern, root="~")
except RuntimeError as e:
QtWidgets.QMessageBox.critical(self, s["search_error_title"], s["search_error_text"].format(e=e))
QtWidgets.QMessageBox.critical(
self, s["search_error_title"], s["search_error_text"].format(e=e)
)
return
finally:
QtWidgets.QApplication.restoreOverrideCursor()
@@ -562,23 +694,37 @@ class MainWindow(QtWidgets.QMainWindow):
self.results_dialog = FileSearchResults(results, self.strings, self)
self.results_dialog.show()
else:
reply = QtWidgets.QMessageBox.question(self, s["no_results_title"], s["no_results_home_text"],
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No, QtWidgets.QMessageBox.StandardButton.No)
reply = QtWidgets.QMessageBox.question(
self,
s["no_results_title"],
s["no_results_home_text"],
QtWidgets.QMessageBox.StandardButton.Yes
| QtWidgets.QMessageBox.StandardButton.No,
QtWidgets.QMessageBox.StandardButton.No,
)
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
try:
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) # type: ignore
results = find(pattern, root='/')
results = find(pattern, root="/")
except RuntimeError as e:
QtWidgets.QMessageBox.critical(self, s["search_error_title"], s["search_error_text"].format(e=e))
QtWidgets.QMessageBox.critical(
self,
s["search_error_title"],
s["search_error_text"].format(e=e),
)
return
finally:
QtWidgets.QApplication.restoreOverrideCursor()
if results:
self.results_dialog = FileSearchResults(results, self.strings, self)
self.results_dialog = FileSearchResults(
results, self.strings, self
)
self.results_dialog.show()
else:
QtWidgets.QMessageBox.information(self, s["no_results_title"], s["no_results_root_text"])
QtWidgets.QMessageBox.information(
self, s["no_results_title"], s["no_results_root_text"]
)
def start_web_search(self):
s = self.strings["web_search"]
@@ -597,14 +743,20 @@ class MainWindow(QtWidgets.QMainWindow):
leta = MullvadLetaWrapper(engine=search_engine)
results = leta.search(query)
if results and results.get('results'):
self.web_results_dialog = WebSearchResults(results, self.strings, self)
if results and results.get("results"):
self.web_results_dialog = WebSearchResults(
results, self.strings, self
)
self.web_results_dialog.show()
else:
QtWidgets.QMessageBox.information(self, s["no_results_title"], s["no_results_text"])
QtWidgets.QMessageBox.information(
self, s["no_results_title"], s["no_results_text"]
)
except Exception as e:
QtWidgets.QMessageBox.critical(self, s["search_error_title"], s["search_error_text"].format(e=e))
QtWidgets.QMessageBox.critical(
self, s["search_error_title"], s["search_error_text"].format(e=e)
)
finally:
QtWidgets.QApplication.restoreOverrideCursor()
@@ -616,13 +768,19 @@ class MainWindow(QtWidgets.QMainWindow):
QtWidgets.QApplication.restoreOverrideCursor()
if not update_available:
QtWidgets.QMessageBox.information(self, s["no_updates_title"], s["no_updates_text"])
QtWidgets.QMessageBox.information(
self, s["no_updates_title"], s["no_updates_text"]
)
return
else:
reply = QtWidgets.QMessageBox.question(self, s["update_available_title"],
reply = QtWidgets.QMessageBox.question(
self,
s["update_available_title"],
s["update_available_text"],
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
QtWidgets.QMessageBox.StandardButton.Yes)
QtWidgets.QMessageBox.StandardButton.Yes
| QtWidgets.QMessageBox.StandardButton.No,
QtWidgets.QMessageBox.StandardButton.Yes,
)
if reply == QtWidgets.QMessageBox.StandardButton.No:
return
@@ -631,15 +789,23 @@ class MainWindow(QtWidgets.QMainWindow):
QtWidgets.QApplication.restoreOverrideCursor()
if status == "UPDATED":
reply = QtWidgets.QMessageBox.question(self, s["update_success_title"],
reply = QtWidgets.QMessageBox.question(
self,
s["update_success_title"],
s["update_success_text"].format(message=message),
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
QtWidgets.QMessageBox.StandardButton.Yes)
QtWidgets.QMessageBox.StandardButton.Yes
| QtWidgets.QMessageBox.StandardButton.No,
QtWidgets.QMessageBox.StandardButton.Yes,
)
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
self.restart_application()
elif status == "FAILED":
QtWidgets.QMessageBox.critical(self, s["update_failed_title"], s["update_failed_text"].format(message=message))
QtWidgets.QMessageBox.critical(
self,
s["update_failed_title"],
s["update_failed_text"].format(message=message),
)
def restart_application(self):
presence.end()