Few small different syntex fixes and so on

This commit is contained in:
N0\A
2025-11-10 11:32:33 +01:00
parent e38b1b3052
commit d4ff7feb17
6 changed files with 757 additions and 408 deletions

View File

@@ -36,4 +36,4 @@ A WIP desktop assistant for Linux and 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
If you want to contribute in any way, PRs (and issues) welcome

View File

@@ -1,126 +1,222 @@
import fnmatch
import os
import sys
import fnmatch
def get_language(file_path):
"""Detect programming language based on file extension."""
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',
".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, '')
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, '/')
rel_path_forward = rel_path.replace(os.sep, "/")
basename = os.path.basename(file_path)
# Exclude specific files
exclude_files = {'.pyc'}
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'
".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):
"""Load patterns from .gitignore file."""
patterns = []
gitignore_path = os.path.join(root_dir, '.gitignore')
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:
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('#'):
if line and not line.startswith("#"):
# Remove trailing backslash for escaped #
if line.startswith(r'\#'):
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):
"""Check if path matches any gitignore pattern (simplified)."""
if not patterns:
return False
# Get relative path with forward slashes
rel_path = os.path.relpath(path, root_dir).replace(os.sep, '/')
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 + '/'
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('!'):
if pattern.startswith("!"):
continue
# Directory pattern (ending with /)
if pattern.endswith('/'):
if pattern.endswith("/"):
if not os.path.isdir(path):
continue
pattern = pattern.rstrip('/')
pattern = pattern.rstrip("/")
# Match directory name or anything inside it
if fnmatch.fnmatch(rel_path, pattern) or fnmatch.fnmatch(rel_path_with_slash, pattern + '/*'):
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 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:
if "/" not in pattern:
# Check basename
basename = os.path.basename(rel_path)
if fnmatch.fnmatch(basename, pattern):
@@ -129,122 +225,140 @@ def is_ignored(path, patterns, root_dir):
# 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):
"""Get all files from a directory, optionally recursively."""
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)
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('.'):
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)):
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('.'):
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)):
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():
"""Main execution function."""
root_dir = os.getcwd()
script_path = os.path.abspath(__file__)
output_file = "copy.md"
codeblock = "```"
# Load .gitignore patterns
gitignore_patterns = load_gitignore_patterns(root_dir)
if gitignore_patterns:
print(f"Loaded {len(gitignore_patterns)} patterns from .gitignore")
def is_output_file(path):
return os.path.abspath(path) == os.path.abspath(output_file)
# Directories to process: (path, recursive)
directories = [
("./", False), # Root directory
("assets/", True), # Archive directory (with subdirectories)
("core/", True), # Legacy directory
("./", False), # Root directory
("assets/", True), # Archive directory (with subdirectories)
("core/", True), # Legacy directory
("strings/", True), # Maybe directory
("windows/", True)
("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]
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 = "# Main website\n\n"
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 += (
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)
@@ -253,5 +367,6 @@ def main():
print(f"Error writing to {output_file}: {e}")
sys.exit(1)
if __name__ == "__main__":
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 []
return []

View File

@@ -1,50 +1,114 @@
# 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
class MullvadLetaWrapper:
"""Wrapper for Mullvad Leta privacy-focused search engine."""
BASE_URL = "https://leta.mullvad.net/search"
# Available search engines
ENGINES = ["brave", "google"]
# 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
TIME_FILTERS = ["d", "w", "m", "y"] # day, week, month, year
def __init__(self, engine: str = "brave"):
"""
Initialize the Mullvad Leta wrapper.
Args:
engine: Search engine to use ("brave" or "google")
"""
if engine not in self.ENGINES:
raise ValueError(f"Engine must be one of {self.ENGINES}")
self.engine = engine
self.session = requests.Session()
def _get_headers(self) -> Dict[str, str]:
"""Get request headers with user agent."""
return {
@@ -59,45 +123,42 @@ 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(
self,
query: str,
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.
Args:
query: Search query string
country: Country code filter (e.g., "us", "uk")
language: Language code filter (e.g., "en", "fr")
last_updated: Time filter ("d", "w", "m", "y")
page: Page number (default: 1)
Returns:
Dictionary containing search results and metadata
"""
if country and country not in self.COUNTRIES:
raise ValueError(f"Invalid country code. Must be one of {self.COUNTRIES}")
if language and language not in self.LANGUAGES:
raise ValueError(f"Invalid language code. Must be one of {self.LANGUAGES}")
if last_updated and last_updated not in self.TIME_FILTERS:
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
if language:
@@ -106,37 +167,37 @@ class MullvadLetaWrapper:
params["lastUpdated"] = last_updated
if page > 1:
params["page"] = str(page)
# Set cookie for engine preference
cookies = {"engine": self.engine}
# Make request
response = self.session.get(
self.BASE_URL,
params=params,
headers=self._get_headers(),
cookies=cookies,
timeout=10
timeout=10,
)
response.raise_for_status()
# Parse results
return self._parse_results(response.text, query, page)
def _parse_results(self, html: str, query: str, page: int) -> Dict[str, Any]:
"""
Parse HTML response and extract search results.
Args:
html: HTML response content
query: Original search query
page: Current page number
Returns:
Dictionary containing parsed results
"""
soup = BeautifulSoup(html, 'html.parser')
soup = BeautifulSoup(html, "html.parser")
results = {
"query": query,
"page": page,
@@ -144,100 +205,102 @@ 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
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}")
return None
def _parse_infobox(self, infobox_div) -> Dict[str, Any]:
"""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']
desc_tag = infobox_div.find('p')
infobox["url"] = url_tag["href"]
desc_tag = infobox_div.find("p")
if desc_tag:
infobox["description"] = desc_tag.get_text(strip=True)
return infobox
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}")
@@ -248,23 +311,23 @@ class MullvadLetaWrapper:
if __name__ == "__main__":
# Create wrapper instance
leta = MullvadLetaWrapper(engine="brave")
# Perform a search
results = leta.search("python programming", country="us", language="en")
# Display results
print(f"Query: {results['query']}")
print(f"Engine: {results['engine']}")
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")
print(f" {news['source']}\n")

51
main.py
View File

@@ -1,39 +1,44 @@
#!/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.setIcon(QtWidgets.QMessageBox.Critical) # type: ignore
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)
app.setApplicationName("CLARA")
restart = "--restart" in sys.argv
@@ -44,28 +49,28 @@ def main():
update_available = is_update_available()
if update_available:
update_repository()
# Start preloading apps in the background
preload_thread = threading.Thread(target=preload_apps, daemon=True)
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)
tcp_port=config.get("dukto_tcp_port", 4644),
)
pet = MainWindow(
dukto_handler=dukto_handler,
strings=strings,
dukto_handler=dukto_handler,
strings=strings,
config=config,
restart=restart,
no_quit=no_quit,
restart=restart,
no_quit=no_quit,
)
if config.get("discord_presence", True):
presence.start()
dukto_handler.initialize()
dukto_handler.say_hello()
@@ -77,4 +82,4 @@ def main():
if __name__ == "__main__":
main()
main()

View File

@@ -1,29 +1,30 @@
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.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()
# Dukto signals
peer_added_signal = QtCore.Signal(Peer)
peer_removed_signal = QtCore.Signal(Peer)
@@ -35,14 +36,15 @@ class MainWindow(QtWidgets.QMainWindow):
send_start_signal = QtCore.Signal(str)
send_complete_signal = QtCore.Signal(list)
dukto_error_signal = QtCore.Signal(str)
# 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
@@ -51,15 +53,15 @@ class MainWindow(QtWidgets.QMainWindow):
self.no_quit = no_quit
flags = (
QtCore.Qt.FramelessWindowHint #type: ignore
| QtCore.Qt.WindowStaysOnTopHint #type: ignore
| QtCore.Qt.Tool #type: ignore
| QtCore.Qt.WindowDoesNotAcceptFocus #type: ignore
QtCore.Qt.FramelessWindowHint # type: ignore
| QtCore.Qt.WindowStaysOnTopHint # type: ignore
| QtCore.Qt.Tool # type: ignore
| QtCore.Qt.WindowDoesNotAcceptFocus # type: ignore
)
self.setWindowFlags(flags)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground) #type: ignore
self.setWindowFlags(flags)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground) # type: ignore
# Load the image
pix = QtGui.QPixmap(str(ASSET))
self.image_size = pix.size()
@@ -68,30 +70,48 @@ class MainWindow(QtWidgets.QMainWindow):
self.label = QtWidgets.QLabel(self)
self.label.setPixmap(pix)
self.label.resize(pix.size())
# Install event filter on the image to handle clicks
self.label.installEventFilter(self)
self.dukto_handler = dukto_handler
self.progress_dialog = None
# 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
self.peer_added_signal.connect(self.update_peer_menus)
self.peer_removed_signal.connect(self.update_peer_menus)
@@ -107,9 +127,9 @@ class MainWindow(QtWidgets.QMainWindow):
self.tray = QtWidgets.QSystemTrayIcon(self)
self.tray.setIcon(QtGui.QIcon(str(ASSET)))
self.build_menus()
# always on top timer
self.stay_on_top_timer = QtCore.QTimer(self)
self.stay_on_top_timer.timeout.connect(self.ensure_on_top)
@@ -117,7 +137,11 @@ 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 showEvent(self, event):
@@ -125,34 +149,34 @@ class MainWindow(QtWidgets.QMainWindow):
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):
@@ -163,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)
@@ -179,30 +209,36 @@ 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)
if self.restart:
right_menu.addAction(s["restart"], self.restart_application)
right_menu.addAction(s["toggle_visibility"], self.toggle_visible)
right_menu.addSeparator()
if not self.no_quit:
right_menu.addAction(s["quit"], QtWidgets.QApplication.quit)
self.tray.setContextMenu(right_menu)
self.tray.activated.connect(self.handle_tray_activated)
self.tray.show()
self.update_peer_menus()
self.update_share_menu_state()
def update_share_menu_state(self):
s_menu = self.strings["main_window"]["right_menu"]
is_sharing = self.http_share.is_running()
has_shared_files = bool(self.http_share.shared_files)
has_shared_text = bool(self.http_share.shared_text)
@@ -214,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()
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())
@@ -259,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_press=hotkey.press, # type: ignore
on_release=hotkey.release, # type: ignore
)
self.listener.start()
@@ -285,17 +332,21 @@ 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_()
def eventFilter(self, obj, event): #type: ignore
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
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
elif event.button() == QtCore.Qt.RightButton: # type: ignore
self.tray.contextMenu().popup(event.globalPosition().toPoint())
return True
return super().eventFilter(obj, event)
@@ -313,76 +364,116 @@ class MainWindow(QtWidgets.QMainWindow):
def update_peer_menus(self):
s_main = self.strings["main_window"]
no_peers_str = s_main["no_peers"]
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)
def start_file_share_browser(self):
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
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"]
info_text = s["share_browser_files_info"].format(count=len(file_paths))
if not self.http_share.shared_text:
self._show_sharing_dialog(url, main_text, info_text)
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"]
is_changing = bool(self.http_share.shared_text)
text, ok = QtWidgets.QInputDialog.getMultiLineText(
self,
s["share_text_browser_dialog_title"],
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):
@@ -398,34 +489,46 @@ 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):
s = self.strings["main_window"]
msg = QtWidgets.QMessageBox(self)
msg.setIcon(QtWidgets.QMessageBox.Information) #type: ignore
msg.setIcon(QtWidgets.QMessageBox.Information) # type: ignore
msg.setWindowTitle(s["share_browser_title"])
msg.setText(main_text)
msg.setInformativeText(f"{s['share_browser_url']}:\n\n{url}\n\n{info_text}")
copy_btn = msg.addButton(s["copy_url"], QtWidgets.QMessageBox.ActionRole) #type: ignore
open_btn = msg.addButton(s["open_browser"], QtWidgets.QMessageBox.ActionRole) #type: ignore
msg.addButton(QtWidgets.QMessageBox.Ok) #type: ignore
copy_btn = msg.addButton(s["copy_url"], QtWidgets.QMessageBox.ActionRole) # type: ignore
open_btn = msg.addButton(s["open_browser"], QtWidgets.QMessageBox.ActionRole) # type: ignore
msg.addButton(QtWidgets.QMessageBox.Ok) # type: ignore
msg.exec()
clicked = msg.clickedButton()
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))
@@ -435,17 +538,20 @@ class MainWindow(QtWidgets.QMainWindow):
self.tray.showMessage(
s["download_notification_title"],
s["download_notification_text"].format(filename=filename, ip=client_ip),
QtWidgets.QSystemTrayIcon.Information, #type: ignore
3000
QtWidgets.QSystemTrayIcon.Information, # type: ignore
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()
@@ -455,17 +561,23 @@ 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.setWindowModality(QtCore.Qt.WindowModal) # type: ignore
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.setWindowModality(QtCore.Qt.WindowModal) # type: ignore
self.progress_dialog.show()
@QtCore.Slot(int, int)
@@ -480,23 +592,28 @@ class MainWindow(QtWidgets.QMainWindow):
self.progress_dialog.setValue(total_size)
self.progress_dialog.close()
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:
receive_dir = str(Path.home() / "Received")
url = QtCore.QUrl.fromLocalFile(receive_dir)
QtGui.QDesktopServices.openUrl(url)
@QtCore.Slot(list)
def handle_send_complete(self, sent_files: list):
if self.progress_dialog:
@@ -504,19 +621,25 @@ class MainWindow(QtWidgets.QMainWindow):
self.progress_dialog.setValue(self.progress_dialog.maximum())
self.progress_dialog.close()
self.progress_dialog = None
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):
if self.progress_dialog:
self.progress_dialog.close()
self.progress_dialog = None
dialog = TextViewerDialog(text, self.strings, self)
dialog.exec()
@@ -525,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)
@@ -547,16 +674,18 @@ class MainWindow(QtWidgets.QMainWindow):
dialog.setWindowTitle(s["input_title"])
dialog.setLabelText(s["input_label"])
dialog.move(QtGui.QCursor.pos())
ok = dialog.exec()
pattern = dialog.textValue()
if ok and pattern:
try:
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) #type: ignore
results = find(pattern, root='~')
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) # type: ignore
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()
@@ -565,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"]
@@ -592,66 +735,86 @@ class MainWindow(QtWidgets.QMainWindow):
ok = dialog.exec()
query = dialog.textValue()
if ok and query:
try:
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) #type: ignore
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) # type: ignore
search_engine = self.config.get("search_engine", "brave")
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()
def update_git(self):
s = self.strings["main_window"]["updater"]
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) #type: ignore
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) # type: ignore
update_available = is_update_available()
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"],
s["update_available_text"],
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
QtWidgets.QMessageBox.StandardButton.Yes)
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,
)
if reply == QtWidgets.QMessageBox.StandardButton.No:
return
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) #type: ignore
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) # type: ignore
status, message = update_repository()
QtWidgets.QApplication.restoreOverrideCursor()
if status == "UPDATED":
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)
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,
)
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()
self.dukto_handler.shutdown()
if self.http_share.is_running():
self.http_share.stop()
args = [sys.executable] + sys.argv
subprocess.Popen(args)
QtWidgets.QApplication.quit()
QtWidgets.QApplication.quit()