From 3b39ed18063eb55a2ebce3538517cd4f344597b8 Mon Sep 17 00:00:00 2001 From: "N0\\A" Date: Thu, 6 Nov 2025 15:11:57 +0100 Subject: [PATCH] wayland try 1 --- SUPERCOPY.py | 1 + core/wayland_utils.py | 107 +++++++++++++++++++++++++++++++++++++++++ main.py | 7 +-- windows/main_window.py | 50 ++++++++++++++++--- 4 files changed, 153 insertions(+), 12 deletions(-) create mode 100644 core/wayland_utils.py diff --git a/SUPERCOPY.py b/SUPERCOPY.py index e7459ee..6e29d3f 100644 --- a/SUPERCOPY.py +++ b/SUPERCOPY.py @@ -7,6 +7,7 @@ files = [ "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", diff --git a/core/wayland_utils.py b/core/wayland_utils.py new file mode 100644 index 0000000..54921a8 --- /dev/null +++ b/core/wayland_utils.py @@ -0,0 +1,107 @@ +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 \ No newline at end of file diff --git a/main.py b/main.py index 4464762..47a9718 100644 --- a/main.py +++ b/main.py @@ -69,12 +69,7 @@ def main(): dukto_handler.initialize() dukto_handler.say_hello() - # 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.position_bottom_right() pet.show() diff --git a/windows/main_window.py b/windows/main_window.py index 1563711..1d1bc9d 100644 --- a/windows/main_window.py +++ b/windows/main_window.py @@ -11,6 +11,7 @@ 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 @@ -46,19 +47,35 @@ 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 - flags = ( - QtCore.Qt.FramelessWindowHint #type: ignore - | QtCore.Qt.WindowStaysOnTopHint #type: ignore - | QtCore.Qt.Tool #type: ignore - | QtCore.Qt.WindowDoesNotAcceptFocus #type: ignore - ) + # 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 + | QtCore.Qt.Tool #type: ignore + | QtCore.Qt.WindowDoesNotAcceptFocus #type: ignore + ) 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) @@ -119,6 +136,22 @@ 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"] @@ -251,10 +284,15 @@ 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