from __future__ import annotations from dataclasses import dataclass from pathlib import Path from typing import Any, Callable, Literal from .desktop_identity import APPLICATION_WM_CLASS, APPLICATION_WM_INSTANCE DialogKind = Literal["info", "success", "warning", "error", "question"] @dataclass(frozen=True) class DialogPalette: """Colors and glyph used by one launcher dialog intent.""" accent: str accent_soft: str glyph: str _DIALOG_PALETTES: dict[str, DialogPalette] = { "info": DialogPalette("#d98927", "#3a2819", "i"), "success": DialogPalette("#55b878", "#173122", "✓"), "warning": DialogPalette("#e0a248", "#3b2a18", "!"), "error": DialogPalette("#cf604c", "#3a1d19", "×"), "question": DialogPalette("#d98927", "#3a2819", "?"), } _BASE_TK_SCALING = 96.0 / 72.0 def dialog_palette(kind: DialogKind) -> DialogPalette: """Return the stable visual palette for a themed launcher dialog.""" try: return _DIALOG_PALETTES[kind] except KeyError as error: raise ValueError(f"unsupported launcher dialog kind: {kind}") from error def square_icon_layout(width: int, height: int) -> tuple[int, int, int]: """Return square canvas size and centered offsets for a window icon.""" if width <= 0 or height <= 0: raise ValueError("icon dimensions must be positive") size = max(width, height) return size, (size - width) // 2, (size - height) // 2 def icon_subsample_factor(width: int, height: int, max_dimension: int) -> int: """Return the integer Tk subsample needed to fit an image within a square.""" if width <= 0 or height <= 0: raise ValueError("icon dimensions must be positive") if max_dimension <= 0: raise ValueError("maximum icon dimension must be positive") largest_dimension = max(width, height) return max(1, (largest_dimension + max_dimension - 1) // max_dimension) def load_square_window_icon(master: Any, path: Path) -> Any: """Load a PNG into a square transparent Tk image for desktop shells.""" import tkinter as tk source = tk.PhotoImage(master=master, file=str(path)) size, offset_x, offset_y = square_icon_layout(source.width(), source.height()) if source.width() == source.height(): return source icon = tk.PhotoImage(master=master, width=size, height=size) icon.tk.call( str(icon), "copy", str(source), "-to", offset_x, offset_y, ) return icon def centered_window_geometry( width: int, height: int, *, parent_x: int, parent_y: int, parent_width: int, parent_height: int, ) -> str: """Return Tk geometry centered over a parent window.""" if width <= 0 or height <= 0: raise ValueError("window dimensions must be positive") x = parent_x + (max(1, parent_width) - width) // 2 y = parent_y + (max(1, parent_height) - height) // 2 return f"{width}x{height}{_geometry_offset(x)}{_geometry_offset(y)}" def dragged_window_geometry( pointer_x: int, pointer_y: int, *, offset_x: int, offset_y: int, ) -> str: """Return the Tk position for a pointer-driven custom title bar.""" return ( f"{_geometry_offset(pointer_x - offset_x)}" f"{_geometry_offset(pointer_y - offset_y)}" ) def widget_ui_scale(widget: Any) -> float: """Return the launcher scale already configured on a Tk interpreter.""" try: scaling = float(widget.tk.call("tk", "scaling")) except (AttributeError, TypeError, ValueError): return 1.0 return min(2.0, max(1.0, scaling / _BASE_TK_SCALING)) def _geometry_offset(value: int) -> str: return f"+{value}" if value >= 0 else str(value) def _bind_window_drag(handle: Any, *widgets: Any) -> None: position = [0, 0] def begin(event: Any) -> None: position[0] = int(event.x_root) - int(handle.winfo_x()) position[1] = int(event.y_root) - int(handle.winfo_y()) def move(event: Any) -> None: handle.geometry( dragged_window_geometry( int(event.x_root), int(event.y_root), offset_x=position[0], offset_y=position[1], ) ) for widget in widgets: widget.bind("", begin, add="+") widget.bind("", move, add="+") class DashboardChrome: """Controller for one launcher window and its custom controls.""" def __init__( self, root: Any, content: Any, close_button: Any, minimize_button: Any, *, managed: bool, identity_configured: bool, allow_close: bool, ) -> None: self.root = root self.content = content self.managed = managed self.identity_configured = identity_configured self._close_button = close_button self._minimize_button = minimize_button self._allow_close = allow_close self._close_command: Callable[[], None] = root.destroy if close_button is not None: close_button.configure(command=self.close) if minimize_button is not None: minimize_button.configure(command=self.minimize) root.bind("", self._close_from_event, add="+") def set_close_command(self, command: Callable[[], None]) -> None: self._close_command = command def close(self) -> None: if self._allow_close: self._close_command() def minimize(self) -> None: """Ask the window manager to iconify the managed dashboard.""" import tkinter as tk try: self.root.iconify() except tk.TclError: pass def _close_from_event(self, _event: Any) -> str: self.close() return "break" def _configure_x11_window( root: Any, *, resource_name: str, resource_class: str, ) -> tuple[bool, bool]: """Hide the X11 frame and give desktop shells a stable window identity.""" import ctypes.util library_path = ctypes.util.find_library("X11") if library_path is None: return False, False try: x11 = ctypes.CDLL(library_path) except OSError: return False, False class XClassHint(ctypes.Structure): _fields_ = [ ("res_name", ctypes.c_char_p), ("res_class", ctypes.c_char_p), ] x11.XOpenDisplay.argtypes = [ctypes.c_char_p] x11.XOpenDisplay.restype = ctypes.c_void_p x11.XQueryTree.argtypes = [ ctypes.c_void_p, ctypes.c_ulong, ctypes.POINTER(ctypes.c_ulong), ctypes.POINTER(ctypes.c_ulong), ctypes.POINTER(ctypes.POINTER(ctypes.c_ulong)), ctypes.POINTER(ctypes.c_uint), ] x11.XQueryTree.restype = ctypes.c_int x11.XInternAtom.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int] x11.XInternAtom.restype = ctypes.c_ulong x11.XChangeProperty.argtypes = [ ctypes.c_void_p, ctypes.c_ulong, ctypes.c_ulong, ctypes.c_ulong, ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_ubyte), ctypes.c_int, ] x11.XChangeProperty.restype = ctypes.c_int x11.XSetClassHint.argtypes = [ ctypes.c_void_p, ctypes.c_ulong, ctypes.POINTER(XClassHint), ] x11.XSetClassHint.restype = ctypes.c_int x11.XFlush.argtypes = [ctypes.c_void_p] x11.XFlush.restype = ctypes.c_int x11.XFree.argtypes = [ctypes.c_void_p] x11.XFree.restype = ctypes.c_int x11.XCloseDisplay.argtypes = [ctypes.c_void_p] x11.XCloseDisplay.restype = ctypes.c_int root.update_idletasks() display = x11.XOpenDisplay(None) if display is None: return False, False children = ctypes.POINTER(ctypes.c_ulong)() try: root_window = ctypes.c_ulong() wrapper_window = ctypes.c_ulong() child_count = ctypes.c_uint() queried = x11.XQueryTree( display, root.winfo_id(), ctypes.byref(root_window), ctypes.byref(wrapper_window), ctypes.byref(children), ctypes.byref(child_count), ) if not queried or not wrapper_window.value: return False, False atom = x11.XInternAtom(display, b"_MOTIF_WM_HINTS", 0) if not atom: return False, False motif_hints_decorations = 1 << 1 hints = (ctypes.c_ulong * 5)(motif_hints_decorations, 0, 0, 0, 0) changed = x11.XChangeProperty( display, wrapper_window.value, atom, atom, 32, 0, ctypes.cast(hints, ctypes.POINTER(ctypes.c_ubyte)), len(hints), ) identity = XClassHint( res_name=resource_name.encode("utf-8"), res_class=resource_class.encode("utf-8"), ) identity_changed = x11.XSetClassHint( display, wrapper_window.value, ctypes.byref(identity), ) x11.XFlush(display) return bool(changed), bool(identity_changed) finally: if children: x11.XFree(children) x11.XCloseDisplay(display) def create_window_chrome( root: Any, *, title: str, subtitle: str, window_icon: Any | None = None, show_title_bar: bool = True, show_close: bool = True, show_minimize: bool = False, allow_close: bool = True, ) -> DashboardChrome: """Install the launcher's custom frame and window controls.""" import tkinter as tk ui_scale = widget_ui_scale(root) managed, identity_configured = _configure_x11_window( root, resource_name=APPLICATION_WM_INSTANCE, resource_class=APPLICATION_WM_CLASS, ) if not managed: root.overrideredirect(True) border = tk.Frame( root, background="#6a482d", borderwidth=0, highlightthickness=0, ) border.pack(fill="both", expand=True) surface = tk.Frame( border, background="#0c0908", borderwidth=0, highlightthickness=0, ) surface.pack(fill="both", expand=True, padx=1, pady=1) title_bar = tk.Frame( surface, background="#17100d", height=round(32 * ui_scale), borderwidth=0, highlightthickness=0, cursor="fleur", ) if show_title_bar: title_bar.pack(fill="x") title_bar.pack_propagate(False) tk_accent = tk.Frame(title_bar, background="#d98927", width=3) tk_accent.pack(side="left", fill="y") title_icon: Any | None = None if window_icon is not None: try: factor = icon_subsample_factor( window_icon.width(), window_icon.height(), round(20 * ui_scale), ) title_icon = window_icon.subsample(factor, factor) except (tk.TclError, ValueError): title_icon = None if title_icon is not None: mark = tk.Label( title_bar, image=title_icon, width=round(22 * ui_scale), height=round(22 * ui_scale), background="#17100d", borderwidth=0, highlightthickness=0, cursor="fleur", ) mark.image = title_icon else: mark = tk.Frame( title_bar, width=round(22 * ui_scale), height=round(22 * ui_scale), background="#17100d", borderwidth=0, highlightthickness=0, cursor="fleur", ) mark.pack(side="left", padx=(9, 7)) identity = tk.Frame(title_bar, background="#17100d", cursor="fleur") identity.pack(side="left", fill="y") title_label = tk.Label( identity, text=title, background="#17100d", foreground="#f2dfbd", font=("Sans", 8, "bold"), cursor="fleur", ) title_label.pack(side="left", pady=(0, 1)) subtitle_label = tk.Label( identity, text=f" · {subtitle}", background="#17100d", foreground="#8f7b69", font=("Sans", 7), cursor="fleur", ) subtitle_label.pack(side="left", pady=(0, 1)) close_button: Any = None if show_close: close_button = tk.Button( title_bar, text="×", background="#17100d", foreground="#c9b7a4", activebackground="#a63f2f", activeforeground="#ffffff", font=("Sans", 12), width=4, borderwidth=0, highlightthickness=2, highlightbackground="#17100d", highlightcolor="#f4bd61", relief="flat", cursor="hand2", takefocus=True, ) close_button.pack(side="right", fill="y") minimize_button: Any = None if show_minimize: minimize_button = tk.Button( title_bar, text="—", background="#17100d", foreground="#c9b7a4", activebackground="#312219", activeforeground="#ffffff", font=("Sans", 10, "bold"), width=4, borderwidth=0, highlightthickness=2, highlightbackground="#17100d", highlightcolor="#f4bd61", relief="flat", cursor="hand2", takefocus=True, ) minimize_button.pack(side="right", fill="y") content = tk.Frame( surface, background="#0c0908", borderwidth=0, highlightthickness=0, ) content.pack(fill="both", expand=True) if show_title_bar: _bind_window_drag(root, title_bar, mark, identity, title_label, subtitle_label) return DashboardChrome( root, content, close_button, minimize_button, managed=managed, identity_configured=identity_configured, allow_close=allow_close, ) def create_dashboard_chrome( root: Any, *, title: str, subtitle: str, window_icon: Any | None = None, ) -> DashboardChrome: """Install the draggable frame and full controls used by the dashboard.""" return create_window_chrome( root, title=title, subtitle=subtitle, window_icon=window_icon, show_close=True, show_minimize=True, allow_close=True, ) def show_themed_dialog( parent: Any, *, title: str, message: str, kind: DialogKind = "info", confirm: bool = False, accept_text: str = "OK", cancel_text: str = "CANCEL", window_icon: Any | None = None, ) -> bool: """Show one custom-framed modal dialog matching the launcher palette.""" import tkinter as tk ui_scale = widget_ui_scale(parent) palette = dialog_palette(kind) dialog = tk.Toplevel(parent) dialog.withdraw() dialog.title(title) dialog.configure(background=palette.accent) dialog.resizable(False, False) dialog.overrideredirect(True) if window_icon is not None: try: dialog.iconphoto(False, window_icon) except tk.TclError: pass try: dialog.transient(parent) except tk.TclError: pass result = False def finish(value: bool) -> None: nonlocal result result = value try: dialog.grab_release() except tk.TclError: pass dialog.destroy() border = tk.Frame(dialog, background=palette.accent) border.pack(fill="both", expand=True) surface = tk.Frame(border, background="#191311") surface.pack(fill="both", expand=True, padx=1, pady=1) header = tk.Frame( surface, background="#24170f", height=round(66 * ui_scale), ) header.pack(fill="x") header.pack_propagate(False) tk_accent = tk.Frame(header, background=palette.accent, width=5) tk_accent.pack(side="left", fill="y") header_copy = tk.Frame(header, background="#24170f") header_copy.pack(side="left", fill="both", expand=True, padx=16, pady=10) eyebrow = tk.Label( header_copy, text="PRISTONTALE EU · LINUX LAUNCHER", background="#24170f", foreground=palette.accent, font=("Sans", 7, "bold"), anchor="w", ) eyebrow.pack(fill="x") title_label = tk.Label( header_copy, text=title, background="#24170f", foreground="#fff0d2", font=("Georgia", 15, "bold"), anchor="w", ) title_label.pack(fill="x", pady=(1, 0)) close_button = tk.Button( header, text="×", command=lambda: finish(False), background="#24170f", foreground="#c9b7a4", activebackground="#a63f2f", activeforeground="#ffffff", font=("Sans", 12), width=4, borderwidth=0, highlightthickness=2, highlightbackground="#24170f", highlightcolor="#f4bd61", relief="flat", cursor="hand2", takefocus=True, ) close_button.pack(side="right", fill="y") body = tk.Frame(surface, background="#191311", padx=20, pady=20) body.pack(fill="both", expand=True) modal_icon_size = round(46 * ui_scale) modal_icon: Any | None = None if window_icon is not None: try: factor = icon_subsample_factor( window_icon.width(), window_icon.height(), modal_icon_size, ) modal_icon = window_icon.subsample(factor, factor) except (tk.TclError, ValueError): modal_icon = None if modal_icon is not None: icon_host = tk.Frame( body, width=modal_icon_size, height=modal_icon_size, background="#191311", borderwidth=0, highlightthickness=0, ) icon_host.pack(side="left", anchor="n", padx=(0, 16)) icon_host.pack_propagate(False) icon = tk.Label( icon_host, image=modal_icon, background="#191311", borderwidth=0, highlightthickness=0, ) icon.place(relx=0.5, rely=0.5, anchor="center") else: icon = tk.Canvas( body, width=modal_icon_size, height=modal_icon_size, background="#191311", borderwidth=0, highlightthickness=0, ) icon.create_oval( round(3 * ui_scale), round(3 * ui_scale), modal_icon_size - round(3 * ui_scale), modal_icon_size - round(3 * ui_scale), fill=palette.accent_soft, outline=palette.accent, width=2, ) icon.create_text( modal_icon_size // 2, modal_icon_size // 2, text=palette.glyph, fill=palette.accent, font=("Sans", 17, "bold"), ) icon.pack(side="left", anchor="n", padx=(0, 16)) message_label = tk.Label( body, text=message, background="#191311", foreground="#ded1c4", font=("Sans", 10), justify="left", anchor="w", wraplength=round(420 * ui_scale), ) message_label.pack(side="left", fill="both", expand=True, anchor="n") footer = tk.Frame(surface, background="#100d0c", padx=18, pady=12) footer.pack(fill="x") def button( container: Any, *, label: str, command: Callable[[], None], primary: bool, ) -> Any: background = palette.accent if primary else "#2a201c" foreground = "#17100d" if primary else "#ead8c2" active_background = "#efae57" if primary else "#3a2b25" return tk.Button( container, text=label, command=command, background=background, foreground=foreground, activebackground=active_background, activeforeground="#ffffff" if not primary else "#17100d", font=("Sans", 8, "bold"), padx=16, pady=7, borderwidth=0, highlightthickness=2, highlightbackground="#100d0c", highlightcolor="#f4bd61", relief="flat", cursor="hand2", ) accept_button = button( footer, label=accept_text, command=lambda: finish(True), primary=True, ) accept_button.pack(side="right") if confirm: cancel_button = button( footer, label=cancel_text, command=lambda: finish(False), primary=False, ) cancel_button.pack(side="right", padx=(0, 8)) _bind_window_drag(dialog, header, header_copy, eyebrow, title_label) dialog.protocol("WM_DELETE_WINDOW", lambda: finish(False)) dialog.bind("", lambda _event: finish(False)) dialog.bind("", lambda _event: finish(True)) parent.update_idletasks() dialog.update_idletasks() width = min(round(560 * ui_scale), max(1, dialog.winfo_screenwidth() - 64)) height = min( max(round(218 * ui_scale), dialog.winfo_reqheight()), max(1, dialog.winfo_screenheight() - 64), ) dialog.geometry( centered_window_geometry( width, height, parent_x=parent.winfo_rootx(), parent_y=parent.winfo_rooty(), parent_width=parent.winfo_width(), parent_height=parent.winfo_height(), ) ) dialog.deiconify() dialog.lift() dialog.grab_set() accept_button.focus_set() parent.wait_window(dialog) return result