diff --git a/README.md b/README.md index 25c3dd5..61d9ec4 100644 --- a/README.md +++ b/README.md @@ -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 \ No newline at end of file +If you want to contribute in any way, PRs (and issues) welcome diff --git a/SUPERCOPY.py b/SUPERCOPY.py index 764ee6f..186d059 100644 --- a/SUPERCOPY.py +++ b/SUPERCOPY.py @@ -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"\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() \ No newline at end of file + main() diff --git a/core/file_search.py b/core/file_search.py index 123b903..f527b42 100644 --- a/core/file_search.py +++ b/core/file_search.py @@ -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 [] \ No newline at end of file + return [] diff --git a/core/web_search.py b/core/web_search.py index 52549f3..1481358 100644 --- a/core/web_search.py +++ b/core/web_search.py @@ -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") \ No newline at end of file + print(f" {news['source']}\n") diff --git a/main.py b/main.py index ee7211f..0b7c9c8 100644 --- a/main.py +++ b/main.py @@ -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() \ No newline at end of file + main() diff --git a/windows/main_window.py b/windows/main_window.py index 53cbc3c..fa0c6b4 100644 --- a/windows/main_window.py +++ b/windows/main_window.py @@ -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() \ No newline at end of file + + QtWidgets.QApplication.quit()