Compare commits
3 Commits
3b39ed1806
...
aa2c23537c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa2c23537c | ||
|
|
69b0fb3226 | ||
|
|
1814054cc2 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -9,6 +9,6 @@ __pycache__/
|
||||
*.pyc.*
|
||||
*.pyo.*
|
||||
|
||||
# SUPERCOPY
|
||||
SUPERCOPY.py
|
||||
copy.md
|
||||
.vscode
|
||||
283
SUPERCOPY.py
283
SUPERCOPY.py
@@ -1,38 +1,257 @@
|
||||
files = [
|
||||
"core/app_launcher.py",
|
||||
"core/config.py",
|
||||
"core/discord_presence.py",
|
||||
"core/dukto.py",
|
||||
"core/file_search.py",
|
||||
"core/headers.py",
|
||||
"core/http_share.py",
|
||||
"core/updater.py",
|
||||
"core/wayland_utils.py",
|
||||
"core/web_search.py",
|
||||
"strings/en.json",
|
||||
"strings/personality_en.json",
|
||||
"windows/app_launcher.py",
|
||||
"windows/calculator.py",
|
||||
"windows/file_search.py",
|
||||
"windows/main_window.py",
|
||||
"windows/text_viewer.py",
|
||||
"windows/web_results.py",
|
||||
"main.py",
|
||||
"README.md",
|
||||
"requirements.txt"
|
||||
import 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',
|
||||
}
|
||||
|
||||
ext = os.path.splitext(file_path)[1].lower()
|
||||
return extension_map.get(ext, '')
|
||||
|
||||
def should_exclude(file_path, root_dir):
|
||||
"""Determine if a file should be excluded from copying."""
|
||||
abs_path = os.path.abspath(file_path)
|
||||
rel_path = os.path.relpath(abs_path, root_dir)
|
||||
rel_path_forward = rel_path.replace(os.sep, '/')
|
||||
basename = os.path.basename(file_path)
|
||||
|
||||
# Exclude specific files
|
||||
exclude_files = {'.pyc'}
|
||||
if rel_path_forward in exclude_files or basename in exclude_files:
|
||||
return True
|
||||
|
||||
# Exclude image files
|
||||
image_extensions = {
|
||||
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.bmp', '.ico',
|
||||
'.tiff', '.tif', '.webp', '.heic', '.heif', '.avif',
|
||||
'.jfif', '.pjpeg', '.pjp', '.tga', '.psd', '.raw',
|
||||
'.cr2', '.nef', '.orf', '.sr2', '.arw', '.dng', '.rw2',
|
||||
'.raf', '.3fr', '.kdc', '.mef', '.mrw', '.pef', '.srw',
|
||||
'.x3f', '.r3d', '.fff', '.iiq', '.erf', '.nrw'
|
||||
}
|
||||
|
||||
ext = os.path.splitext(file_path)[1].lower()
|
||||
if ext in image_extensions:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def load_gitignore_patterns(root_dir):
|
||||
"""Load patterns from .gitignore file."""
|
||||
patterns = []
|
||||
gitignore_path = os.path.join(root_dir, '.gitignore')
|
||||
|
||||
if os.path.isfile(gitignore_path):
|
||||
try:
|
||||
with open(gitignore_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
# Skip empty lines and comments
|
||||
if line and not line.startswith('#'):
|
||||
# Remove trailing backslash for escaped #
|
||||
if line.startswith(r'\#'):
|
||||
line = line[1:]
|
||||
patterns.append(line)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not read .gitignore: {e}")
|
||||
|
||||
return patterns
|
||||
|
||||
def is_ignored(path, patterns, root_dir):
|
||||
"""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, '/')
|
||||
|
||||
# For directories, also check with trailing slash
|
||||
if os.path.isdir(path):
|
||||
rel_path_with_slash = rel_path + '/'
|
||||
else:
|
||||
rel_path_with_slash = rel_path
|
||||
|
||||
for pattern in patterns:
|
||||
# Skip negation patterns (too complex for this script)
|
||||
if pattern.startswith('!'):
|
||||
continue
|
||||
|
||||
# Directory pattern (ending with /)
|
||||
if pattern.endswith('/'):
|
||||
if not os.path.isdir(path):
|
||||
continue
|
||||
pattern = pattern.rstrip('/')
|
||||
# Match directory name or anything inside it
|
||||
if fnmatch.fnmatch(rel_path, pattern) or fnmatch.fnmatch(rel_path_with_slash, pattern + '/*'):
|
||||
return True
|
||||
continue
|
||||
|
||||
# Absolute pattern (starting with /) - match from root only
|
||||
if pattern.startswith('/'):
|
||||
pattern = pattern.lstrip('/')
|
||||
if fnmatch.fnmatch(rel_path, pattern):
|
||||
return True
|
||||
continue
|
||||
|
||||
# Pattern without slash - matches at any level
|
||||
if '/' not in pattern:
|
||||
# Check basename
|
||||
basename = os.path.basename(rel_path)
|
||||
if fnmatch.fnmatch(basename, pattern):
|
||||
return True
|
||||
else:
|
||||
# Pattern with slash - relative path match
|
||||
if fnmatch.fnmatch(rel_path, pattern):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_files_from_directory(directory, recursive=False, root_dir=None, gitignore_patterns=None):
|
||||
"""Get all files from a directory, optionally recursively."""
|
||||
if root_dir is None:
|
||||
root_dir = os.getcwd()
|
||||
|
||||
if gitignore_patterns is None:
|
||||
gitignore_patterns = []
|
||||
|
||||
files_list = []
|
||||
abs_directory = os.path.abspath(directory)
|
||||
|
||||
if not os.path.exists(abs_directory):
|
||||
print(f"Warning: Directory '{directory}' not found.")
|
||||
return files_list
|
||||
|
||||
# Skip if directory itself is ignored
|
||||
if is_ignored(abs_directory, gitignore_patterns, root_dir):
|
||||
return files_list
|
||||
|
||||
if recursive:
|
||||
for dirpath, dirnames, filenames in os.walk(abs_directory):
|
||||
# Filter directories: exclude hidden and gitignored
|
||||
dirnames[:] = [
|
||||
d for d in dirnames
|
||||
if not d.startswith('.') and not is_ignored(os.path.join(dirpath, d), gitignore_patterns, root_dir)
|
||||
]
|
||||
|
||||
# Filter files
|
||||
for filename in filenames:
|
||||
if filename.startswith('.'):
|
||||
continue
|
||||
|
||||
full_path = os.path.join(dirpath, filename)
|
||||
if (os.path.isfile(full_path) and
|
||||
not should_exclude(full_path, root_dir) and
|
||||
not is_ignored(full_path, gitignore_patterns, root_dir)):
|
||||
files_list.append(full_path)
|
||||
else:
|
||||
for filename in os.listdir(abs_directory):
|
||||
if filename.startswith('.'):
|
||||
continue
|
||||
|
||||
full_path = os.path.join(abs_directory, filename)
|
||||
|
||||
# Skip directories in non-recursive mode
|
||||
if os.path.isdir(full_path):
|
||||
continue
|
||||
|
||||
if (os.path.isfile(full_path) and
|
||||
not should_exclude(full_path, root_dir) and
|
||||
not is_ignored(full_path, gitignore_patterns, root_dir)):
|
||||
files_list.append(full_path)
|
||||
|
||||
return files_list
|
||||
|
||||
def main():
|
||||
"""Main execution function."""
|
||||
root_dir = os.getcwd()
|
||||
script_path = os.path.abspath(__file__)
|
||||
output_file = "copy.md"
|
||||
codeblock = "```"
|
||||
|
||||
copy = ""
|
||||
# Load .gitignore patterns
|
||||
gitignore_patterns = load_gitignore_patterns(root_dir)
|
||||
if gitignore_patterns:
|
||||
print(f"Loaded {len(gitignore_patterns)} patterns from .gitignore")
|
||||
|
||||
for file in files:
|
||||
with open(file, "r", encoding="utf-8") as f:
|
||||
lines = f.readlines()
|
||||
copy += f"### {file}\n\n"
|
||||
copy += f"{codeblock}python\n"
|
||||
copy += "".join(lines)
|
||||
copy += f"\n{codeblock}\n\n"
|
||||
def is_output_file(path):
|
||||
return os.path.abspath(path) == os.path.abspath(output_file)
|
||||
|
||||
with open("copy.md", "w", encoding="utf-8") as f:
|
||||
f.write(copy)
|
||||
# Directories to process: (path, recursive)
|
||||
directories = [
|
||||
("./", False), # Root directory
|
||||
("assets/", True), # Archive directory (with subdirectories)
|
||||
("core/", True), # Legacy directory
|
||||
("strings/", True), # Maybe directory
|
||||
("windows/", True)
|
||||
]
|
||||
|
||||
all_files = []
|
||||
for directory, recursive in directories:
|
||||
files = get_files_from_directory(directory, recursive, root_dir, gitignore_patterns)
|
||||
files = [f for f in files if not is_output_file(f) and os.path.abspath(f) != script_path]
|
||||
all_files.extend(files)
|
||||
|
||||
# Remove duplicates and sort
|
||||
all_files = sorted(set(all_files))
|
||||
|
||||
markdown_content = "# Main website\n\n"
|
||||
file_count = 0
|
||||
|
||||
for file_path in all_files:
|
||||
try:
|
||||
rel_path = os.path.relpath(file_path, root_dir)
|
||||
language = get_language(file_path)
|
||||
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
markdown_content += f"### {rel_path.replace(os.sep, '/')}\n\n"
|
||||
markdown_content += f"{codeblock}{language}\n" if language else f"{codeblock}\n"
|
||||
markdown_content += content
|
||||
markdown_content += f"\n{codeblock}\n\n"
|
||||
|
||||
file_count += 1
|
||||
|
||||
except UnicodeDecodeError:
|
||||
print(f"Warning: Could not read {file_path} as text. Skipping.")
|
||||
except Exception as e:
|
||||
print(f"Error processing {file_path}: {e}")
|
||||
|
||||
markdown_content += f"<!-- Processed {file_count} files -->\n"
|
||||
|
||||
try:
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
f.write(markdown_content)
|
||||
print(f"Successfully created {output_file} with {file_count} files.")
|
||||
except Exception as e:
|
||||
print(f"Error writing to {output_file}: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,107 +0,0 @@
|
||||
import os
|
||||
import subprocess
|
||||
from typing import Tuple, Optional
|
||||
|
||||
def is_wayland() -> bool:
|
||||
session_type = os.environ.get('XDG_SESSION_TYPE', '').lower()
|
||||
wayland_display = os.environ.get('WAYLAND_DISPLAY', '')
|
||||
|
||||
return session_type == 'wayland' or bool(wayland_display)
|
||||
|
||||
def get_screen_info() -> Tuple[int, int]:
|
||||
if is_wayland():
|
||||
try:
|
||||
output = subprocess.check_output(['wlr-randr'], text=True, stderr=subprocess.DEVNULL)
|
||||
for line in output.splitlines():
|
||||
if 'current' in line.lower():
|
||||
parts = line.split()
|
||||
for part in parts:
|
||||
if 'x' in part and part.replace('x', '').replace('px', '').isdigit():
|
||||
dims = part.replace('px', '').split('x')
|
||||
return int(dims[0]), int(dims[1])
|
||||
except (subprocess.CalledProcessError, FileNotFoundError, PermissionError):
|
||||
pass
|
||||
|
||||
import json
|
||||
|
||||
try:
|
||||
output = subprocess.check_output(['swaymsg', '-t', 'get_outputs'], text=True)
|
||||
outputs = json.loads(output)
|
||||
if outputs and outputs[0].get('current_mode'):
|
||||
mode = outputs[0]['current_mode']
|
||||
return mode['width'], mode['height']
|
||||
except (subprocess.CalledProcessError, FileNotFoundError, json.JSONDecodeError, KeyError, PermissionError):
|
||||
pass
|
||||
|
||||
try:
|
||||
output = subprocess.check_output(['kscreen-doctor', '-o'], text=True)
|
||||
for line in output.splitlines():
|
||||
if 'Output:' in line:
|
||||
continue
|
||||
if 'x' in line and '@' in line:
|
||||
resolution = line.split('@')[0].strip().split()[-1]
|
||||
if 'x' in resolution:
|
||||
dims = resolution.split('x')
|
||||
return int(dims[0]), int(dims[1])
|
||||
except (subprocess.CalledProcessError, FileNotFoundError, PermissionError):
|
||||
pass
|
||||
|
||||
try:
|
||||
output = subprocess.check_output(['xrandr'], text=True, stderr=subprocess.DEVNULL)
|
||||
for line in output.splitlines():
|
||||
if ' connected' in line and 'primary' in line:
|
||||
parts = line.split()
|
||||
for part in parts:
|
||||
if 'x' in part and '+' in part:
|
||||
dims = part.split('+')[0].split('x')
|
||||
return int(dims[0]), int(dims[1])
|
||||
elif ' connected' in line and '*' in line:
|
||||
parts = line.split()
|
||||
for i, part in enumerate(parts):
|
||||
if 'x' in part and i > 0:
|
||||
dims = part.split('x')
|
||||
if dims[0].isdigit():
|
||||
return int(dims[0]), int(dims[1].split('+')[0] if '+' in dims[1] else dims[1])
|
||||
except (subprocess.CalledProcessError, FileNotFoundError, PermissionError):
|
||||
pass
|
||||
|
||||
# maybe somehow right sometimes
|
||||
return 1920, 1080
|
||||
|
||||
def set_window_bottom_right_wayland(window, width: int, height: int):
|
||||
screen_w, screen_h = get_screen_info()
|
||||
x = screen_w - width
|
||||
y = screen_h - height
|
||||
|
||||
try:
|
||||
window.move(x, y)
|
||||
except:
|
||||
pass
|
||||
|
||||
def get_wayland_compositor() -> Optional[str]:
|
||||
desktop = os.environ.get('XDG_CURRENT_DESKTOP', '').lower()
|
||||
|
||||
if 'sway' in desktop:
|
||||
return 'sway'
|
||||
elif 'kde' in desktop or 'plasma' in desktop:
|
||||
return 'kwin'
|
||||
elif 'gnome' in desktop:
|
||||
return 'mutter'
|
||||
elif 'hypr' in desktop:
|
||||
return 'hyprland'
|
||||
|
||||
# detect from process list
|
||||
try:
|
||||
output = subprocess.check_output(['ps', 'aux'], text=True)
|
||||
if 'sway' in output:
|
||||
return 'sway'
|
||||
elif 'kwin_wayland' in output:
|
||||
return 'kwin'
|
||||
elif 'gnome-shell' in output:
|
||||
return 'mutter'
|
||||
elif 'Hyprland' in output:
|
||||
return 'hyprland'
|
||||
except:
|
||||
pass
|
||||
|
||||
return None
|
||||
7
main.py
7
main.py
@@ -69,7 +69,12 @@ def main():
|
||||
dukto_handler.initialize()
|
||||
dukto_handler.say_hello()
|
||||
|
||||
pet.position_bottom_right()
|
||||
# bottom right corner
|
||||
screen_geometry = app.primaryScreen().availableGeometry()
|
||||
pet_geometry = pet.frameGeometry()
|
||||
x = screen_geometry.width() - pet_geometry.width()
|
||||
y = screen_geometry.height() - pet_geometry.height()
|
||||
pet.move(x, y)
|
||||
|
||||
pet.show()
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ from core.file_search import find
|
||||
from core.web_search import MullvadLetaWrapper
|
||||
from core.http_share import FileShareServer
|
||||
from core.config import Config
|
||||
from core.wayland_utils import is_wayland, get_screen_info
|
||||
|
||||
from windows.app_launcher import AppLauncherDialog
|
||||
from windows.file_search import FileSearchResults
|
||||
@@ -47,21 +46,10 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
self.strings = strings
|
||||
self.config = config
|
||||
self.listener = None
|
||||
self.is_wayland = is_wayland()
|
||||
|
||||
self.restart = restart
|
||||
self.no_quit = no_quit
|
||||
|
||||
# Configure window flags based on display server
|
||||
if self.is_wayland:
|
||||
# Wayland-compatible flags
|
||||
flags = (
|
||||
QtCore.Qt.FramelessWindowHint #type: ignore
|
||||
| QtCore.Qt.WindowStaysOnTopHint #type: ignore
|
||||
| QtCore.Qt.Tool #type: ignore
|
||||
)
|
||||
else:
|
||||
# X11 flags
|
||||
flags = (
|
||||
QtCore.Qt.FramelessWindowHint #type: ignore
|
||||
| QtCore.Qt.WindowStaysOnTopHint #type: ignore
|
||||
@@ -71,11 +59,6 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
|
||||
self.setWindowFlags(flags)
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground) #type: ignore
|
||||
|
||||
# On Wayland, set additional window properties to prevent Alt+Tab
|
||||
if self.is_wayland:
|
||||
self.setAttribute(QtCore.Qt.WA_X11NetWmWindowTypeUtility) #type: ignore
|
||||
|
||||
pix = QtGui.QPixmap(str(ASSET))
|
||||
|
||||
self.label = QtWidgets.QLabel(self)
|
||||
@@ -136,22 +119,6 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
if config.get("hotkey") != None and config.get("hotkey") != "none" and config.get("hotkey") != "":
|
||||
self.start_hotkey_listener()
|
||||
|
||||
def position_bottom_right(self):
|
||||
"""Position window at bottom right corner."""
|
||||
if self.is_wayland:
|
||||
# On Wayland, get screen info and position window
|
||||
screen_w, screen_h = get_screen_info()
|
||||
x = screen_w - self.width()
|
||||
y = screen_h - self.height()
|
||||
self.move(x, y)
|
||||
else:
|
||||
# On X11, use Qt's screen geometry
|
||||
screen_geometry = QtWidgets.QApplication.primaryScreen().availableGeometry()
|
||||
pet_geometry = self.frameGeometry()
|
||||
x = screen_geometry.width() - pet_geometry.width()
|
||||
y = screen_geometry.height() - pet_geometry.height()
|
||||
self.move(x, y)
|
||||
|
||||
def build_menus(self):
|
||||
s = self.strings["main_window"]["right_menu"]
|
||||
|
||||
@@ -284,15 +251,10 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
def ensure_on_top(self):
|
||||
if self.isVisible() and not self.left_menu.isVisible() and not self.tray.contextMenu().isVisible():
|
||||
self.raise_()
|
||||
# Re-apply window flags to ensure staying on top on Wayland
|
||||
if self.is_wayland:
|
||||
self.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint, True) #type: ignore
|
||||
|
||||
def showEvent(self, event):
|
||||
super().showEvent(event)
|
||||
self.raise_()
|
||||
# Ensure bottom right positioning
|
||||
QtCore.QTimer.singleShot(100, self.position_bottom_right)
|
||||
|
||||
def mousePressEvent(self, event: QtGui.QMouseEvent):
|
||||
if event.button() == QtCore.Qt.LeftButton: #type: ignore
|
||||
|
||||
Reference in New Issue
Block a user