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

@@ -1,59 +1,153 @@
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()
@@ -62,20 +156,20 @@ def should_exclude(file_path, root_dir):
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:
@@ -83,44 +177,46 @@ def load_gitignore_patterns(root_dir):
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):
@@ -132,8 +228,10 @@ def is_ignored(path, patterns, root_dir):
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()
@@ -155,23 +253,29 @@ def get_files_from_directory(directory, recursive=False, root_dir=None, gitignor
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)
@@ -180,15 +284,17 @@ def get_files_from_directory(directory, recursive=False, root_dir=None, gitignor
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"
@@ -204,23 +310,29 @@ def main():
# 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:
@@ -232,7 +344,9 @@ def main():
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"
@@ -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,7 +1,10 @@
# 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
@@ -15,18 +18,79 @@ class MullvadLetaWrapper:
# 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
@@ -59,7 +123,7 @@ 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(
@@ -68,7 +132,7 @@ class MullvadLetaWrapper:
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.
@@ -93,10 +157,7 @@ class MullvadLetaWrapper:
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
@@ -116,7 +177,7 @@ class MullvadLetaWrapper:
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()
@@ -135,7 +196,7 @@ class MullvadLetaWrapper:
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,
@@ -144,37 +205,37 @@ 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
@@ -182,19 +243,19 @@ class MullvadLetaWrapper:
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}")
@@ -204,19 +265,19 @@ class MullvadLetaWrapper:
"""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)
@@ -225,19 +286,21 @@ class MullvadLetaWrapper:
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}")
@@ -258,13 +321,13 @@ if __name__ == "__main__":
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")

27
main.py
View File

@@ -1,35 +1,40 @@
#!/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)
@@ -52,7 +57,7 @@ def main():
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(

View File

@@ -1,26 +1,27 @@
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()
@@ -39,8 +40,9 @@ class MainWindow(QtWidgets.QMainWindow):
# 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
@@ -51,14 +53,14 @@ 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))
@@ -78,18 +80,36 @@ class MainWindow(QtWidgets.QMainWindow):
# 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
@@ -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):
@@ -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,9 +209,15 @@ 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)
@@ -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)
@@ -316,35 +367,64 @@ class MainWindow(QtWidgets.QMainWindow):
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)
@@ -352,8 +432,12 @@ class MainWindow(QtWidgets.QMainWindow):
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
@@ -361,7 +445,12 @@ class MainWindow(QtWidgets.QMainWindow):
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"]
@@ -372,7 +461,9 @@ 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 start_text_share_browser(self): def start_text_share_browser(self):
s = self.strings["main_window"] s = self.strings["main_window"]
@@ -382,7 +473,7 @@ class MainWindow(QtWidgets.QMainWindow):
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,26 +489,33 @@ 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()
@@ -425,7 +523,12 @@ class MainWindow(QtWidgets.QMainWindow):
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)
@@ -482,14 +594,19 @@ class MainWindow(QtWidgets.QMainWindow):
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:
@@ -507,9 +624,15 @@ class MainWindow(QtWidgets.QMainWindow):
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):
@@ -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)
@@ -553,10 +680,12 @@ class MainWindow(QtWidgets.QMainWindow):
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"]
@@ -595,54 +738,74 @@ class MainWindow(QtWidgets.QMainWindow):
if ok and query: if ok and query:
try: try:
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) #type: ignore QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) # type: ignore
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()