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

View File

@@ -1,8 +1,8 @@
import fnmatch
import os
import shutil import shutil
import subprocess import subprocess
import os
import platform
import fnmatch
def _find_native(pattern: str, root: str): def _find_native(pattern: str, root: str):
"""Native Python implementation of file search using os.walk.""" """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)) results.append(os.path.join(dirpath, filename))
return results return results
def find(pattern: str, root: str='/'):
def find(pattern: str, root: str = "/"):
path = os.path.expanduser(root) path = os.path.expanduser(root)
if shutil.which('fd') is None: if shutil.which("fd") is None:
return _find_native(f"*{pattern}*", path) return _find_native(f"*{pattern}*", path)
else: else:
try: 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() return out.splitlines()
except subprocess.CalledProcessError: 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 import requests
from typing import Optional, Dict, List, Any
from urllib.parse import urlencode
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from core.headers import get_useragent from core.headers import get_useragent
class MullvadLetaWrapper: class MullvadLetaWrapper:
"""Wrapper for Mullvad Leta privacy-focused search engine.""" """Wrapper for Mullvad Leta privacy-focused search engine."""
BASE_URL = "https://leta.mullvad.net/search" BASE_URL = "https://leta.mullvad.net/search"
# Available search engines # Available search engines
ENGINES = ["brave", "google"] ENGINES = ["brave", "google"]
# Available countries (from the HTML) # Available countries (from the HTML)
COUNTRIES = [ COUNTRIES = [
"ar", "au", "at", "be", "br", "ca", "cl", "cn", "dk", "fi", "ar",
"fr", "de", "hk", "in", "id", "it", "jp", "kr", "my", "mx", "au",
"nl", "nz", "no", "ph", "pl", "pt", "ru", "sa", "za", "es", "at",
"se", "ch", "tw", "tr", "uk", "us" "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 # Available languages
LANGUAGES = [ LANGUAGES = [
"ar", "bg", "ca", "zh-hans", "zh-hant", "hr", "cs", "da", "nl", "ar",
"en", "et", "fi", "fr", "de", "he", "hu", "is", "it", "jp", "bg",
"ko", "lv", "lt", "nb", "pl", "pt", "ro", "ru", "sr", "sk", "ca",
"sl", "es", "sv", "tr" "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
TIME_FILTERS = ["d", "w", "m", "y"] # day, week, month, year TIME_FILTERS = ["d", "w", "m", "y"] # day, week, month, year
def __init__(self, engine: str = "brave"): def __init__(self, engine: str = "brave"):
""" """
Initialize the Mullvad Leta wrapper. Initialize the Mullvad Leta wrapper.
Args: Args:
engine: Search engine to use ("brave" or "google") engine: Search engine to use ("brave" or "google")
""" """
if engine not in self.ENGINES: if engine not in self.ENGINES:
raise ValueError(f"Engine must be one of {self.ENGINES}") raise ValueError(f"Engine must be one of {self.ENGINES}")
self.engine = engine self.engine = engine
self.session = requests.Session() self.session = requests.Session()
def _get_headers(self) -> Dict[str, str]: def _get_headers(self) -> Dict[str, str]:
"""Get request headers with user agent.""" """Get request headers with user agent."""
return { return {
@@ -59,45 +123,42 @@ class MullvadLetaWrapper:
"sec-fetch-site": "same-origin", "sec-fetch-site": "same-origin",
"sec-fetch-user": "?1", "sec-fetch-user": "?1",
"upgrade-insecure-requests": "1", "upgrade-insecure-requests": "1",
"user-agent": get_useragent() "user-agent": get_useragent(),
} }
def search( def search(
self, self,
query: str, query: str,
country: Optional[str] = None, country: Optional[str] = None,
language: Optional[str] = None, language: Optional[str] = None,
last_updated: Optional[str] = None, last_updated: Optional[str] = None,
page: int = 1 page: int = 1,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" """
Perform a search on Mullvad Leta. Perform a search on Mullvad Leta.
Args: Args:
query: Search query string query: Search query string
country: Country code filter (e.g., "us", "uk") country: Country code filter (e.g., "us", "uk")
language: Language code filter (e.g., "en", "fr") language: Language code filter (e.g., "en", "fr")
last_updated: Time filter ("d", "w", "m", "y") last_updated: Time filter ("d", "w", "m", "y")
page: Page number (default: 1) page: Page number (default: 1)
Returns: Returns:
Dictionary containing search results and metadata Dictionary containing search results and metadata
""" """
if country and country not in self.COUNTRIES: if country and country not in self.COUNTRIES:
raise ValueError(f"Invalid country code. Must be one of {self.COUNTRIES}") raise ValueError(f"Invalid country code. Must be one of {self.COUNTRIES}")
if language and language not in self.LANGUAGES: if language and language not in self.LANGUAGES:
raise ValueError(f"Invalid language code. Must be one of {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: if last_updated and last_updated not in self.TIME_FILTERS:
raise ValueError(f"Invalid time filter. Must be one of {self.TIME_FILTERS}") raise ValueError(f"Invalid time filter. Must be one of {self.TIME_FILTERS}")
# Build query parameters # Build query parameters
params = { params = {"q": query, "engine": self.engine}
"q": query,
"engine": self.engine
}
if country: if country:
params["country"] = country params["country"] = country
if language: if language:
@@ -106,37 +167,37 @@ class MullvadLetaWrapper:
params["lastUpdated"] = last_updated params["lastUpdated"] = last_updated
if page > 1: if page > 1:
params["page"] = str(page) params["page"] = str(page)
# Set cookie for engine preference # Set cookie for engine preference
cookies = {"engine": self.engine} cookies = {"engine": self.engine}
# Make request # Make request
response = self.session.get( response = self.session.get(
self.BASE_URL, self.BASE_URL,
params=params, params=params,
headers=self._get_headers(), headers=self._get_headers(),
cookies=cookies, cookies=cookies,
timeout=10 timeout=10,
) )
response.raise_for_status() response.raise_for_status()
# Parse results # Parse results
return self._parse_results(response.text, query, page) return self._parse_results(response.text, query, page)
def _parse_results(self, html: str, query: str, page: int) -> Dict[str, Any]: def _parse_results(self, html: str, query: str, page: int) -> Dict[str, Any]:
""" """
Parse HTML response and extract search results. Parse HTML response and extract search results.
Args: Args:
html: HTML response content html: HTML response content
query: Original search query query: Original search query
page: Current page number page: Current page number
Returns: Returns:
Dictionary containing parsed results Dictionary containing parsed results
""" """
soup = BeautifulSoup(html, 'html.parser') soup = BeautifulSoup(html, "html.parser")
results = { results = {
"query": query, "query": query,
"page": page, "page": page,
@@ -144,100 +205,102 @@ class MullvadLetaWrapper:
"results": [], "results": [],
"infobox": None, "infobox": None,
"news": [], "news": [],
"cached": False "cached": False,
} }
# Check if cached # Check if cached
cache_notice = soup.find('p', class_='small') cache_notice = soup.find("p", class_="small")
if cache_notice and 'cached' in cache_notice.text.lower(): if cache_notice and "cached" in cache_notice.text.lower():
results["cached"] = True results["cached"] = True
# Extract regular search results # Extract regular search results
articles = soup.find_all('article', class_='svelte-fmlk7p') articles = soup.find_all("article", class_="svelte-fmlk7p")
for article in articles: for article in articles:
result = self._parse_article(article) result = self._parse_article(article)
if result: if result:
results["results"].append(result) results["results"].append(result)
# Extract infobox if present # Extract infobox if present
infobox_div = soup.find('div', class_='infobox') infobox_div = soup.find("div", class_="infobox")
if infobox_div: if infobox_div:
results["infobox"] = self._parse_infobox(infobox_div) results["infobox"] = self._parse_infobox(infobox_div)
# Extract news results # Extract news results
news_div = soup.find('div', class_='news') news_div = soup.find("div", class_="news")
if news_div: if news_div:
news_articles = news_div.find_all('article') news_articles = news_div.find_all("article")
for article in news_articles: for article in news_articles:
news_item = self._parse_news_article(article) news_item = self._parse_news_article(article)
if news_item: if news_item:
results["news"].append(news_item) results["news"].append(news_item)
# Check for next page # 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 results["has_next_page"] = next_button is not None
return results return results
def _parse_article(self, article) -> Optional[Dict[str, str]]: def _parse_article(self, article) -> Optional[Dict[str, str]]:
"""Parse a single search result article.""" """Parse a single search result article."""
try: try:
link_tag = article.find('a', href=True) link_tag = article.find("a", href=True)
if not link_tag: if not link_tag:
return None return None
title_tag = article.find('h3') title_tag = article.find("h3")
snippet_tag = article.find('p', class_='result__body') snippet_tag = article.find("p", class_="result__body")
cite_tag = article.find('cite') cite_tag = article.find("cite")
return { return {
"url": link_tag['href'], "url": link_tag["href"],
"title": title_tag.get_text(strip=True) if title_tag else "", "title": title_tag.get_text(strip=True) if title_tag else "",
"snippet": snippet_tag.get_text(strip=True) if snippet_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: except Exception as e:
print(f"Error parsing article: {e}") print(f"Error parsing article: {e}")
return None return None
def _parse_infobox(self, infobox_div) -> Dict[str, Any]: def _parse_infobox(self, infobox_div) -> Dict[str, Any]:
"""Parse infobox information.""" """Parse infobox information."""
infobox = {} infobox = {}
title_tag = infobox_div.find('h1') title_tag = infobox_div.find("h1")
if title_tag: if title_tag:
infobox["title"] = title_tag.get_text(strip=True) infobox["title"] = title_tag.get_text(strip=True)
subtitle_tag = infobox_div.find('h2') subtitle_tag = infobox_div.find("h2")
if subtitle_tag: if subtitle_tag:
infobox["subtitle"] = subtitle_tag.get_text(strip=True) 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: 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: if desc_tag:
infobox["description"] = desc_tag.get_text(strip=True) infobox["description"] = desc_tag.get_text(strip=True)
return infobox return infobox
def _parse_news_article(self, article) -> Optional[Dict[str, str]]: def _parse_news_article(self, article) -> Optional[Dict[str, str]]:
"""Parse a news article.""" """Parse a news article."""
try: try:
link_tag = article.find('a', href=True) link_tag = article.find("a", href=True)
if not link_tag: if not link_tag:
return None return None
title_tag = link_tag.find('h3') title_tag = link_tag.find("h3")
cite_tag = link_tag.find('cite') cite_tag = link_tag.find("cite")
time_tag = link_tag.find('time') time_tag = link_tag.find("time")
return { return {
"url": link_tag['href'], "url": link_tag["href"],
"title": title_tag.get_text(strip=True) if title_tag else "", "title": title_tag.get_text(strip=True) if title_tag else "",
"source": cite_tag.get_text(strip=True) if cite_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: except Exception as e:
print(f"Error parsing news article: {e}") print(f"Error parsing news article: {e}")
@@ -248,23 +311,23 @@ class MullvadLetaWrapper:
if __name__ == "__main__": if __name__ == "__main__":
# Create wrapper instance # Create wrapper instance
leta = MullvadLetaWrapper(engine="brave") leta = MullvadLetaWrapper(engine="brave")
# Perform a search # Perform a search
results = leta.search("python programming", country="us", language="en") results = leta.search("python programming", country="us", language="en")
# Display results # Display results
print(f"Query: {results['query']}") print(f"Query: {results['query']}")
print(f"Engine: {results['engine']}") print(f"Engine: {results['engine']}")
print(f"Cached: {results['cached']}") print(f"Cached: {results['cached']}")
print(f"\nFound {len(results['results'])} results:\n") 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"{i}. {result['title']}")
print(f" URL: {result['url']}") print(f" URL: {result['url']}")
print(f" {result['snippet'][:100]}...\n") print(f" {result['snippet'][:100]}...\n")
if results['news']: if results["news"]:
print(f"\nNews ({len(results['news'])} items):") 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['title']}")
print(f" {news['source']}\n") print(f" {news['source']}\n")

51
main.py
View File

@@ -1,39 +1,44 @@
#!/usr/bin/python3 #!/usr/bin/python3
import sys, json import json
from pathlib import Path import sys
from PySide6 import QtWidgets
import threading 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.app_launcher import list_apps
from core.config import config 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 from windows.main_window import MainWindow
STRINGS_PATH = Path(__file__).parent / "strings" / "personality_en.json" STRINGS_PATH = Path(__file__).parent / "strings" / "personality_en.json"
def preload_apps(): def preload_apps():
print("Preloading application list...") print("Preloading application list...")
list_apps() list_apps()
print("Application list preloaded.") print("Application list preloaded.")
def main(): def main():
app = QtWidgets.QApplication(sys.argv) app = QtWidgets.QApplication(sys.argv)
try: 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) strings = json.load(f)
except (FileNotFoundError, json.JSONDecodeError) as e: except (FileNotFoundError, json.JSONDecodeError) as e:
print(f"Error loading strings file: {e}") print(f"Error loading strings file: {e}")
error_dialog = QtWidgets.QMessageBox() error_dialog = QtWidgets.QMessageBox()
error_dialog.setIcon(QtWidgets.QMessageBox.Critical) #type: ignore 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.setWindowTitle("Fatal Error")
error_dialog.exec() error_dialog.exec()
sys.exit(1) sys.exit(1)
app.setApplicationName("CLARA") app.setApplicationName("CLARA")
restart = "--restart" in sys.argv restart = "--restart" in sys.argv
@@ -44,28 +49,28 @@ def main():
update_available = is_update_available() update_available = is_update_available()
if update_available: if update_available:
update_repository() update_repository()
# Start preloading apps in the background # Start preloading apps in the background
preload_thread = threading.Thread(target=preload_apps, daemon=True) preload_thread = threading.Thread(target=preload_apps, daemon=True)
preload_thread.start() preload_thread.start()
dukto_handler = DuktoProtocol() dukto_handler = DuktoProtocol()
dukto_handler.set_ports( dukto_handler.set_ports(
udp_port=config.get("dukto_udp_port", 4644), 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( pet = MainWindow(
dukto_handler=dukto_handler, dukto_handler=dukto_handler,
strings=strings, strings=strings,
config=config, config=config,
restart=restart, restart=restart,
no_quit=no_quit, no_quit=no_quit,
) )
if config.get("discord_presence", True): if config.get("discord_presence", True):
presence.start() presence.start()
dukto_handler.initialize() dukto_handler.initialize()
dukto_handler.say_hello() dukto_handler.say_hello()
@@ -77,4 +82,4 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -1,29 +1,30 @@
from PySide6 import QtCore, QtGui, QtWidgets
from pathlib import Path
import subprocess import subprocess
from pynput import keyboard
import sys 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.discord_presence import presence
from core.dukto import Peer from core.dukto import Peer
from core.file_search import find from core.file_search import find
from core.web_search import MullvadLetaWrapper
from core.http_share import FileShareServer 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.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.calculator import CalculatorDialog
from windows.config_window import ConfigWindow 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" ASSET = Path(__file__).parent.parent / "assets" / "2ktan.png"
class MainWindow(QtWidgets.QMainWindow): class MainWindow(QtWidgets.QMainWindow):
show_menu_signal = QtCore.Signal() show_menu_signal = QtCore.Signal()
# Dukto signals # Dukto signals
peer_added_signal = QtCore.Signal(Peer) peer_added_signal = QtCore.Signal(Peer)
peer_removed_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_start_signal = QtCore.Signal(str)
send_complete_signal = QtCore.Signal(list) send_complete_signal = QtCore.Signal(list)
dukto_error_signal = QtCore.Signal(str) dukto_error_signal = QtCore.Signal(str)
# HTTP share signals # HTTP share signals
http_download_signal = QtCore.Signal(str, str) http_download_signal = QtCore.Signal(str, str)
def __init__(
def __init__(self, dukto_handler, strings, config: Config, restart=False, no_quit=False): self, dukto_handler, strings, config: Config, restart=False, no_quit=False
):
super().__init__() super().__init__()
self.strings = strings self.strings = strings
self.config = config self.config = config
self.listener = None self.listener = None
@@ -51,15 +53,15 @@ class MainWindow(QtWidgets.QMainWindow):
self.no_quit = no_quit self.no_quit = no_quit
flags = ( flags = (
QtCore.Qt.FramelessWindowHint #type: ignore QtCore.Qt.FramelessWindowHint # type: ignore
| QtCore.Qt.WindowStaysOnTopHint #type: ignore | QtCore.Qt.WindowStaysOnTopHint # type: ignore
| QtCore.Qt.Tool #type: ignore | QtCore.Qt.Tool # type: ignore
| QtCore.Qt.WindowDoesNotAcceptFocus #type: ignore | QtCore.Qt.WindowDoesNotAcceptFocus # type: ignore
) )
self.setWindowFlags(flags) self.setWindowFlags(flags)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground) #type: ignore self.setAttribute(QtCore.Qt.WA_TranslucentBackground) # type: ignore
# Load the image # Load the image
pix = QtGui.QPixmap(str(ASSET)) pix = QtGui.QPixmap(str(ASSET))
self.image_size = pix.size() self.image_size = pix.size()
@@ -68,30 +70,48 @@ class MainWindow(QtWidgets.QMainWindow):
self.label = QtWidgets.QLabel(self) self.label = QtWidgets.QLabel(self)
self.label.setPixmap(pix) self.label.setPixmap(pix)
self.label.resize(pix.size()) self.label.resize(pix.size())
# Install event filter on the image to handle clicks # Install event filter on the image to handle clicks
self.label.installEventFilter(self) self.label.installEventFilter(self)
self.dukto_handler = dukto_handler self.dukto_handler = dukto_handler
self.progress_dialog = None self.progress_dialog = None
# HTTP file sharing # HTTP file sharing
http_port = self.config.get("http_share_port", 8080) http_port = self.config.get("http_share_port", 8080)
self.http_share = FileShareServer(port=http_port) self.http_share = FileShareServer(port=http_port)
self.http_share.on_download = lambda filename, ip: self.http_download_signal.emit(filename, ip) self.http_share.on_download = (
lambda filename, ip: self.http_download_signal.emit(filename, ip)
)
# Connect Dukto callbacks to emit signals # Connect Dukto callbacks to emit signals
self.dukto_handler.on_peer_added = lambda peer: self.peer_added_signal.emit(peer) self.dukto_handler.on_peer_added = lambda peer: self.peer_added_signal.emit(
self.dukto_handler.on_peer_removed = lambda peer: self.peer_removed_signal.emit(peer) 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_peer_removed = lambda peer: self.peer_removed_signal.emit(
self.dukto_handler.on_receive_start = lambda ip: self.receive_start_signal.emit(ip) peer
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_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_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) self.dukto_handler.on_error = lambda msg: self.dukto_error_signal.emit(msg)
# Connect signals to GUI slots # Connect signals to GUI slots
self.peer_added_signal.connect(self.update_peer_menus) self.peer_added_signal.connect(self.update_peer_menus)
self.peer_removed_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 = QtWidgets.QSystemTrayIcon(self)
self.tray.setIcon(QtGui.QIcon(str(ASSET))) self.tray.setIcon(QtGui.QIcon(str(ASSET)))
self.build_menus() self.build_menus()
# always on top timer # always on top timer
self.stay_on_top_timer = QtCore.QTimer(self) self.stay_on_top_timer = QtCore.QTimer(self)
self.stay_on_top_timer.timeout.connect(self.ensure_on_top) self.stay_on_top_timer.timeout.connect(self.ensure_on_top)
@@ -117,7 +137,11 @@ class MainWindow(QtWidgets.QMainWindow):
# Super key # Super key
self.show_menu_signal.connect(self.show_menu) 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() self.start_hotkey_listener()
def showEvent(self, event): def showEvent(self, event):
@@ -125,34 +149,34 @@ class MainWindow(QtWidgets.QMainWindow):
screen = QtWidgets.QApplication.primaryScreen() screen = QtWidgets.QApplication.primaryScreen()
screen_geometry = screen.availableGeometry() screen_geometry = screen.availableGeometry()
self.setGeometry(screen_geometry) self.setGeometry(screen_geometry)
# Position the image bottom right # Position the image bottom right
label_x = screen_geometry.width() - self.image_size.width() label_x = screen_geometry.width() - self.image_size.width()
label_y = screen_geometry.height() - self.image_size.height() label_y = screen_geometry.height() - self.image_size.height()
self.label.move(label_x, label_y) self.label.move(label_x, label_y)
# Create a mask for the window based on the image position # Create a mask for the window based on the image position
self.update_mask() self.update_mask()
self.raise_() self.raise_()
def update_mask(self): def update_mask(self):
# Create a mask that only includes the image area # Create a mask that only includes the image area
mask = QtGui.QRegion(0, 0, 0, 0) # Empty (duh, it says 0) mask = QtGui.QRegion(0, 0, 0, 0) # Empty (duh, it says 0)
# Add the image region # Add the image region
image_rect = self.label.geometry() image_rect = self.label.geometry()
pixmap = self.label.pixmap() pixmap = self.label.pixmap()
if pixmap and not pixmap.isNull(): if pixmap and not pixmap.isNull():
img = pixmap.toImage() img = pixmap.toImage()
alpha_mask = img.createAlphaMask() alpha_mask = img.createAlphaMask()
bitmap_mask = QtGui.QBitmap.fromImage(alpha_mask) bitmap_mask = QtGui.QBitmap.fromImage(alpha_mask)
image_region = QtGui.QRegion(bitmap_mask) image_region = QtGui.QRegion(bitmap_mask)
image_region.translate(image_rect.x(), image_rect.y()) image_region.translate(image_rect.x(), image_rect.y())
mask = mask.united(image_region) mask = mask.united(image_region)
self.setMask(mask) self.setMask(mask)
def build_menus(self): 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["launch_app"], self.start_app_launcher)
self.left_menu.addAction(s["search_files"], self.start_file_search) 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["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() self.left_menu.addSeparator()
share_menu_left = self.left_menu.addMenu(s["share_menu"]) 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.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() self.left_menu.addSeparator()
# RIGHT MENU (Tray icon) # RIGHT MENU (Tray icon)
@@ -179,30 +209,36 @@ class MainWindow(QtWidgets.QMainWindow):
right_menu.addAction(s.get("calculator", "Calculator"), self.start_calculator) right_menu.addAction(s.get("calculator", "Calculator"), self.start_calculator)
right_menu.addSeparator() right_menu.addSeparator()
share_menu_right = right_menu.addMenu(s["share_menu"]) share_menu_right = right_menu.addMenu(s["share_menu"])
self.share_files_submenu_right = share_menu_right.addMenu(s["share_files_submenu"]) self.share_files_submenu_right = share_menu_right.addMenu(
self.share_text_submenu_right = share_menu_right.addMenu(s["share_text_submenu"]) s["share_files_submenu"]
self.stop_share_action_right = share_menu_right.addAction("Stop Browser Share", self.stop_browser_share) )
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.addSeparator()
right_menu.addAction(s.get("settings", "Settings"), self.start_config_window) right_menu.addAction(s.get("settings", "Settings"), self.start_config_window)
right_menu.addAction(s["check_updates"], self.update_git) right_menu.addAction(s["check_updates"], self.update_git)
if self.restart: if self.restart:
right_menu.addAction(s["restart"], self.restart_application) right_menu.addAction(s["restart"], self.restart_application)
right_menu.addAction(s["toggle_visibility"], self.toggle_visible) right_menu.addAction(s["toggle_visibility"], self.toggle_visible)
right_menu.addSeparator() right_menu.addSeparator()
if not self.no_quit: if not self.no_quit:
right_menu.addAction(s["quit"], QtWidgets.QApplication.quit) right_menu.addAction(s["quit"], QtWidgets.QApplication.quit)
self.tray.setContextMenu(right_menu) self.tray.setContextMenu(right_menu)
self.tray.activated.connect(self.handle_tray_activated) self.tray.activated.connect(self.handle_tray_activated)
self.tray.show() self.tray.show()
self.update_peer_menus() self.update_peer_menus()
self.update_share_menu_state() self.update_share_menu_state()
def update_share_menu_state(self): def update_share_menu_state(self):
s_menu = self.strings["main_window"]["right_menu"] s_menu = self.strings["main_window"]["right_menu"]
is_sharing = self.http_share.is_running() is_sharing = self.http_share.is_running()
has_shared_files = bool(self.http_share.shared_files) has_shared_files = bool(self.http_share.shared_files)
has_shared_text = bool(self.http_share.shared_text) has_shared_text = bool(self.http_share.shared_text)
@@ -214,34 +250,43 @@ class MainWindow(QtWidgets.QMainWindow):
# Configure file share menus # Configure file share menus
for menu in [self.share_files_submenu_left, self.share_files_submenu_right]: for menu in [self.share_files_submenu_left, self.share_files_submenu_right]:
for action in menu.actions(): for action in menu.actions():
if hasattr(action, 'is_browser_action'): if hasattr(action, "is_browser_action"):
menu.removeAction(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 = menu.addAction(action_text)
browser_action.is_browser_action = True browser_action.is_browser_action = True
browser_action.triggered.connect(self.start_file_share_browser) 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()): if not any(a.isSeparator() for a in menu.actions()):
menu.addSeparator() menu.addSeparator()
# Configure text share menus # Configure text share menus
for menu in [self.share_text_submenu_left, self.share_text_submenu_right]: for menu in [self.share_text_submenu_left, self.share_text_submenu_right]:
for action in menu.actions(): for action in menu.actions():
if hasattr(action, 'is_browser_action'): if hasattr(action, "is_browser_action"):
menu.removeAction(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 = menu.addAction(action_text)
browser_action.is_browser_action = True browser_action.is_browser_action = True
browser_action.triggered.connect(self.start_text_share_browser) 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()): if not any(a.isSeparator() for a in menu.actions()):
menu.addSeparator() menu.addSeparator()
def show_menu(self): def show_menu(self):
self.left_menu.popup(QtGui.QCursor.pos()) self.left_menu.popup(QtGui.QCursor.pos())
@@ -259,17 +304,19 @@ class MainWindow(QtWidgets.QMainWindow):
} }
try: try:
keys = [key_map.get(k.strip().lower(), k.strip().lower()) for k in hotkey_str.split('+')] keys = [
formatted_hotkey = '<' + '>+<'.join(keys) + '>' #type: ignore key_map.get(k.strip().lower(), k.strip().lower())
for k in hotkey_str.split("+")
]
formatted_hotkey = "<" + ">+<".join(keys) + ">" # type: ignore
hotkey = keyboard.HotKey( hotkey = keyboard.HotKey(
keyboard.HotKey.parse(formatted_hotkey), keyboard.HotKey.parse(formatted_hotkey), on_activate
on_activate
) )
self.listener = keyboard.Listener( self.listener = keyboard.Listener(
on_press=hotkey.press, #type: ignore on_press=hotkey.press, # type: ignore
on_release=hotkey.release #type: ignore on_release=hotkey.release, # type: ignore
) )
self.listener.start() self.listener.start()
@@ -285,17 +332,21 @@ class MainWindow(QtWidgets.QMainWindow):
super().closeEvent(event) super().closeEvent(event)
def ensure_on_top(self): 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_() self.raise_()
def eventFilter(self, obj, event): #type: ignore def eventFilter(self, obj, event): # type: ignore
# Handle mouse events on the image # Handle mouse events on the image
if obj == self.label: if obj == self.label:
if event.type() == QtCore.QEvent.MouseButtonPress: #type: ignore if event.type() == QtCore.QEvent.MouseButtonPress: # type: ignore
if event.button() == QtCore.Qt.LeftButton: #type: ignore if event.button() == QtCore.Qt.LeftButton: # type: ignore
self.left_menu.popup(event.globalPosition().toPoint()) self.left_menu.popup(event.globalPosition().toPoint())
return True 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()) self.tray.contextMenu().popup(event.globalPosition().toPoint())
return True return True
return super().eventFilter(obj, event) return super().eventFilter(obj, event)
@@ -313,76 +364,116 @@ class MainWindow(QtWidgets.QMainWindow):
def update_peer_menus(self): def update_peer_menus(self):
s_main = self.strings["main_window"] s_main = self.strings["main_window"]
no_peers_str = s_main["no_peers"] no_peers_str = s_main["no_peers"]
peers = list(self.dukto_handler.peers.values()) 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]: for menu in [
actions_to_remove = [a for a in menu.actions() if not a.isSeparator() and not hasattr(a, 'is_browser_action')] 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: for action in actions_to_remove:
menu.removeAction(action) menu.removeAction(action)
if not peers: 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 = menu.addAction(no_peers_str)
action.setEnabled(False) action.setEnabled(False)
else: else:
for peer in sorted(peers, key=lambda p: p.signature): 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) file_action = files_menu.addAction(peer.signature)
text_action = text_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)) file_action.triggered.connect(
text_action.triggered.connect(lambda checked=False, p=peer: self.start_text_send(p)) 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() self.update_share_menu_state()
def start_file_send(self, peer: Peer): def start_file_send(self, peer: Peer):
dialog_title = self.strings["main_window"]["send_files_dialog_title"].format(peer_signature=peer.signature) dialog_title = self.strings["main_window"]["send_files_dialog_title"].format(
file_paths, _ = QtWidgets.QFileDialog.getOpenFileNames(self, dialog_title, str(Path.home())) peer_signature=peer.signature
)
file_paths, _ = QtWidgets.QFileDialog.getOpenFileNames(
self, dialog_title, str(Path.home())
)
if file_paths: if file_paths:
self.dukto_handler.send_file(peer.address, file_paths, peer.port) self.dukto_handler.send_file(peer.address, file_paths, peer.port)
def start_text_send(self, peer: Peer): 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"] 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: if ok and text:
self.dukto_handler.send_text(peer.address, text, peer.port) self.dukto_handler.send_text(peer.address, text, peer.port)
def start_file_share_browser(self): def start_file_share_browser(self):
s = self.strings["main_window"] s = self.strings["main_window"]
is_adding = bool(self.http_share.shared_files) is_adding = bool(self.http_share.shared_files)
dialog_title = "Select files to add" if is_adding else s["share_browser_dialog_title"] dialog_title = (
file_paths, _ = QtWidgets.QFileDialog.getOpenFileNames(self, dialog_title, str(Path.home())) "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: if not file_paths:
return return
try: try:
if is_adding: if is_adding:
self.http_share.add_files(file_paths) 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: else:
url = self.http_share.share_files(file_paths) url = self.http_share.share_files(file_paths)
main_text = s["share_browser_text_files"] main_text = s["share_browser_text_files"]
info_text = s["share_browser_files_info"].format(count=len(file_paths)) info_text = s["share_browser_files_info"].format(count=len(file_paths))
if not self.http_share.shared_text: if not self.http_share.shared_text:
self._show_sharing_dialog(url, main_text, info_text) self._show_sharing_dialog(url, main_text, info_text)
self.update_share_menu_state() self.update_share_menu_state()
except Exception as e: 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): def start_text_share_browser(self):
s = self.strings["main_window"] s = self.strings["main_window"]
is_changing = bool(self.http_share.shared_text) is_changing = bool(self.http_share.shared_text)
text, ok = QtWidgets.QInputDialog.getMultiLineText( text, ok = QtWidgets.QInputDialog.getMultiLineText(
self, self,
s["share_text_browser_dialog_title"], s["share_text_browser_dialog_title"],
s["share_text_browser_dialog_label"], s["share_text_browser_dialog_label"],
self.http_share.shared_text or "" self.http_share.shared_text or "",
) )
if not (ok and text): if not (ok and text):
@@ -398,34 +489,46 @@ class MainWindow(QtWidgets.QMainWindow):
self.update_share_menu_state() self.update_share_menu_state()
except Exception as e: 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): def stop_browser_share(self):
s = self.strings["main_window"] s = self.strings["main_window"]
if self.http_share.is_running(): if self.http_share.is_running():
self.http_share.stop() 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() self.update_share_menu_state()
def _show_sharing_dialog(self, url: str, main_text: str, info_text: str): def _show_sharing_dialog(self, url: str, main_text: str, info_text: str):
s = self.strings["main_window"] s = self.strings["main_window"]
msg = QtWidgets.QMessageBox(self) 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.setWindowTitle(s["share_browser_title"])
msg.setText(main_text) msg.setText(main_text)
msg.setInformativeText(f"{s['share_browser_url']}:\n\n{url}\n\n{info_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 copy_btn = msg.addButton(s["copy_url"], QtWidgets.QMessageBox.ActionRole) # type: ignore
open_btn = msg.addButton(s["open_browser"], 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.addButton(QtWidgets.QMessageBox.Ok) # type: ignore
msg.exec() msg.exec()
clicked = msg.clickedButton() clicked = msg.clickedButton()
if clicked == copy_btn: if clicked == copy_btn:
clipboard = QtWidgets.QApplication.clipboard() clipboard = QtWidgets.QApplication.clipboard()
clipboard.setText(url) 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: elif clicked == open_btn:
QtGui.QDesktopServices.openUrl(QtCore.QUrl(url)) QtGui.QDesktopServices.openUrl(QtCore.QUrl(url))
@@ -435,17 +538,20 @@ class MainWindow(QtWidgets.QMainWindow):
self.tray.showMessage( self.tray.showMessage(
s["download_notification_title"], s["download_notification_title"],
s["download_notification_text"].format(filename=filename, ip=client_ip), s["download_notification_text"].format(filename=filename, ip=client_ip),
QtWidgets.QSystemTrayIcon.Information, #type: ignore QtWidgets.QSystemTrayIcon.Information, # type: ignore
3000 3000,
) )
def show_receive_confirmation(self, sender_ip: str): def show_receive_confirmation(self, sender_ip: str):
reply = QtWidgets.QMessageBox.question( reply = QtWidgets.QMessageBox.question(
self, self,
self.strings["main_window"]["receive_confirm_title"], self.strings["main_window"]["receive_confirm_title"],
self.strings["main_window"]["receive_confirm_text"].format(sender_ip=sender_ip), self.strings["main_window"]["receive_confirm_text"].format(
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No, sender_ip=sender_ip
QtWidgets.QMessageBox.StandardButton.No ),
QtWidgets.QMessageBox.StandardButton.Yes
| QtWidgets.QMessageBox.StandardButton.No,
QtWidgets.QMessageBox.StandardButton.No,
) )
if reply == QtWidgets.QMessageBox.StandardButton.Yes: if reply == QtWidgets.QMessageBox.StandardButton.Yes:
self.dukto_handler.approve_transfer() self.dukto_handler.approve_transfer()
@@ -455,17 +561,23 @@ class MainWindow(QtWidgets.QMainWindow):
@QtCore.Slot(str) @QtCore.Slot(str)
def handle_receive_start(self, sender_ip: str): def handle_receive_start(self, sender_ip: str):
s = self.strings["main_window"]["progress_dialog"] s = self.strings["main_window"]["progress_dialog"]
self.progress_dialog = QtWidgets.QProgressDialog(s["receiving_label"], s["cancel_button"], 0, 100, self) self.progress_dialog = QtWidgets.QProgressDialog(
self.progress_dialog.setWindowTitle(s["receiving_title"].format(sender_ip=sender_ip)) s["receiving_label"], s["cancel_button"], 0, 100, self
self.progress_dialog.setWindowModality(QtCore.Qt.WindowModal) # type: ignore )
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() self.progress_dialog.show()
@QtCore.Slot(str) @QtCore.Slot(str)
def handle_send_start(self, dest_ip: str): def handle_send_start(self, dest_ip: str):
s = self.strings["main_window"]["progress_dialog"] 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.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() self.progress_dialog.show()
@QtCore.Slot(int, int) @QtCore.Slot(int, int)
@@ -480,23 +592,28 @@ class MainWindow(QtWidgets.QMainWindow):
self.progress_dialog.setValue(total_size) self.progress_dialog.setValue(total_size)
self.progress_dialog.close() self.progress_dialog.close()
self.progress_dialog = None self.progress_dialog = None
s = self.strings["main_window"] 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( reply = QtWidgets.QMessageBox.question(
self, self,
s["open_folder_title"], s["open_folder_title"],
s["open_folder_text"], s["open_folder_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.Yes: if reply == QtWidgets.QMessageBox.StandardButton.Yes:
receive_dir = str(Path.home() / "Received") receive_dir = str(Path.home() / "Received")
url = QtCore.QUrl.fromLocalFile(receive_dir) url = QtCore.QUrl.fromLocalFile(receive_dir)
QtGui.QDesktopServices.openUrl(url) QtGui.QDesktopServices.openUrl(url)
@QtCore.Slot(list) @QtCore.Slot(list)
def handle_send_complete(self, sent_files: list): def handle_send_complete(self, sent_files: list):
if self.progress_dialog: if self.progress_dialog:
@@ -504,19 +621,25 @@ class MainWindow(QtWidgets.QMainWindow):
self.progress_dialog.setValue(self.progress_dialog.maximum()) self.progress_dialog.setValue(self.progress_dialog.maximum())
self.progress_dialog.close() self.progress_dialog.close()
self.progress_dialog = None self.progress_dialog = None
s = self.strings["main_window"] s = self.strings["main_window"]
if sent_files and sent_files[0] == "___DUKTO___TEXT___": 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: 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) @QtCore.Slot(str, int)
def handle_receive_text(self, text: str, total_size: int): def handle_receive_text(self, text: str, total_size: int):
if self.progress_dialog: if self.progress_dialog:
self.progress_dialog.close() self.progress_dialog.close()
self.progress_dialog = None self.progress_dialog = None
dialog = TextViewerDialog(text, self.strings, self) dialog = TextViewerDialog(text, self.strings, self)
dialog.exec() dialog.exec()
@@ -525,7 +648,11 @@ class MainWindow(QtWidgets.QMainWindow):
if self.progress_dialog: if self.progress_dialog:
self.progress_dialog.close() self.progress_dialog.close()
self.progress_dialog = None 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): def start_app_launcher(self):
self.app_launcher_dialog = AppLauncherDialog(self.strings, self) self.app_launcher_dialog = AppLauncherDialog(self.strings, self)
@@ -547,16 +674,18 @@ class MainWindow(QtWidgets.QMainWindow):
dialog.setWindowTitle(s["input_title"]) dialog.setWindowTitle(s["input_title"])
dialog.setLabelText(s["input_label"]) dialog.setLabelText(s["input_label"])
dialog.move(QtGui.QCursor.pos()) dialog.move(QtGui.QCursor.pos())
ok = dialog.exec() ok = dialog.exec()
pattern = dialog.textValue() pattern = dialog.textValue()
if ok and pattern: if ok and pattern:
try: try:
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) #type: ignore QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) # type: ignore
results = find(pattern, root='~') results = find(pattern, root="~")
except RuntimeError as e: 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 return
finally: finally:
QtWidgets.QApplication.restoreOverrideCursor() QtWidgets.QApplication.restoreOverrideCursor()
@@ -565,23 +694,37 @@ class MainWindow(QtWidgets.QMainWindow):
self.results_dialog = FileSearchResults(results, self.strings, self) self.results_dialog = FileSearchResults(results, self.strings, self)
self.results_dialog.show() self.results_dialog.show()
else: else:
reply = QtWidgets.QMessageBox.question(self, s["no_results_title"], s["no_results_home_text"], reply = QtWidgets.QMessageBox.question(
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No, QtWidgets.QMessageBox.StandardButton.No) 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: if reply == QtWidgets.QMessageBox.StandardButton.Yes:
try: try:
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) # type: ignore QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) # type: ignore
results = find(pattern, root='/') results = find(pattern, root="/")
except RuntimeError as e: 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 return
finally: finally:
QtWidgets.QApplication.restoreOverrideCursor() QtWidgets.QApplication.restoreOverrideCursor()
if results: if results:
self.results_dialog = FileSearchResults(results, self.strings, self) self.results_dialog = FileSearchResults(
results, self.strings, self
)
self.results_dialog.show() self.results_dialog.show()
else: 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): def start_web_search(self):
s = self.strings["web_search"] s = self.strings["web_search"]
@@ -592,66 +735,86 @@ class MainWindow(QtWidgets.QMainWindow):
ok = dialog.exec() ok = dialog.exec()
query = dialog.textValue() query = dialog.textValue()
if ok and query: if ok and query:
try: try:
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) #type: ignore QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) # type: ignore
search_engine = self.config.get("search_engine", "brave") search_engine = self.config.get("search_engine", "brave")
leta = MullvadLetaWrapper(engine=search_engine) leta = MullvadLetaWrapper(engine=search_engine)
results = leta.search(query) results = leta.search(query)
if results and results.get('results'): if results and results.get("results"):
self.web_results_dialog = WebSearchResults(results, self.strings, self) self.web_results_dialog = WebSearchResults(
results, self.strings, self
)
self.web_results_dialog.show() self.web_results_dialog.show()
else: 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: 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: finally:
QtWidgets.QApplication.restoreOverrideCursor() QtWidgets.QApplication.restoreOverrideCursor()
def update_git(self): def update_git(self):
s = self.strings["main_window"]["updater"] 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() update_available = is_update_available()
QtWidgets.QApplication.restoreOverrideCursor() QtWidgets.QApplication.restoreOverrideCursor()
if not update_available: 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 return
else: else:
reply = QtWidgets.QMessageBox.question(self, s["update_available_title"], reply = QtWidgets.QMessageBox.question(
s["update_available_text"], self,
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No, s["update_available_title"],
QtWidgets.QMessageBox.StandardButton.Yes) s["update_available_text"],
QtWidgets.QMessageBox.StandardButton.Yes
| QtWidgets.QMessageBox.StandardButton.No,
QtWidgets.QMessageBox.StandardButton.Yes,
)
if reply == QtWidgets.QMessageBox.StandardButton.No: if reply == QtWidgets.QMessageBox.StandardButton.No:
return return
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) #type: ignore QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) # type: ignore
status, message = update_repository() status, message = update_repository()
QtWidgets.QApplication.restoreOverrideCursor() QtWidgets.QApplication.restoreOverrideCursor()
if status == "UPDATED": if status == "UPDATED":
reply = QtWidgets.QMessageBox.question(self, s["update_success_title"], reply = QtWidgets.QMessageBox.question(
s["update_success_text"].format(message=message), self,
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No, s["update_success_title"],
QtWidgets.QMessageBox.StandardButton.Yes) 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: if reply == QtWidgets.QMessageBox.StandardButton.Yes:
self.restart_application() self.restart_application()
elif status == "FAILED": 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): def restart_application(self):
presence.end() presence.end()
self.dukto_handler.shutdown() self.dukto_handler.shutdown()
if self.http_share.is_running(): if self.http_share.is_running():
self.http_share.stop() self.http_share.stop()
args = [sys.executable] + sys.argv args = [sys.executable] + sys.argv
subprocess.Popen(args) subprocess.Popen(args)
QtWidgets.QApplication.quit() QtWidgets.QApplication.quit()