#!/usr/bin/env python3
"""LinuxDoors GNOME 51 Resolution -- change the resolution of THIS
headless GNOME 51 (VNC) desktop from inside it, without needing the
outer x2goclient window's own "Change Resolution..." Session-menu
action. Runs entirely locally: no SSH, no outer x2goclient involvement
at all -- this window is itself already running inside the exact same
user session linuxdoors-gnome51-vnc needs (XDG_RUNTIME_DIR/
DBUS_SESSION_BUS_ADDRESS are already set correctly, being inherited
straight from this very GNOME 51 session).

A headless Mutter virtual monitor has exactly one fixed mode for its
whole lifetime (see the long comment on ONMainWindow::
slotGnome51VncChangeResolution() in the patched x2goclient for the full
story) -- so, same as that action, this can only ever restart the
desktop at a new size, never resize it live in place. Applying a change
here WILL close whatever is open in this session, including this
window itself a moment later.
"""
import os
import re
import subprocess

import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib

VNC_SCRIPT = "/opt/gnome51/bin/linuxdoors-gnome51-vnc"
PRESETS = ["1280x720", "1600x900", "1920x1080", "2560x1440", "3840x2160"]
STATE_FILE = os.path.join(
    os.environ.get("XDG_RUNTIME_DIR", "/tmp"),
    "linuxdoors-gnome51-vnc-geometry",
)
GEOM_RE = re.compile(r"^\d+x\d+$")


def current_geometry():
    try:
        with open(STATE_FILE) as f:
            g = f.read().strip()
            return g if GEOM_RE.match(g) else None
    except OSError:
        return None


class ResolutionWindow(Gtk.Window):
    def __init__(self):
        super().__init__(title="GNOME 51 Resolution")
        self.set_border_width(16)
        self.set_default_size(360, -1)
        self.set_resizable(False)

        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
        self.add(box)

        cur = current_geometry()
        self.info = Gtk.Label(
            label=f"Current: {cur}" if cur else "Current resolution: unknown",
            xalign=0,
        )
        box.pack_start(self.info, False, False, 0)

        note = Gtk.Label(
            label=(
                "Changing this restarts the GNOME 51 desktop at the new "
                "size -- anything open in it (including this window) will "
                "close, and any connected VNC viewer will briefly "
                "disconnect and reconnect on its own."
            ),
            xalign=0,
        )
        note.set_line_wrap(True)
        box.pack_start(note, False, False, 0)

        self.combo = Gtk.ComboBoxText()
        items = list(PRESETS)
        if cur and cur not in items:
            items.insert(0, cur)
        for p in items:
            self.combo.append_text(p)
        self.combo.set_active(items.index(cur) if cur in items else 0)
        box.pack_start(self.combo, False, False, 0)

        entry_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
        entry_box.pack_start(Gtk.Label(label="Custom:"), False, False, 0)
        self.custom_entry = Gtk.Entry()
        self.custom_entry.set_placeholder_text("e.g. 1680x1050")
        entry_box.pack_start(self.custom_entry, True, True, 0)
        box.pack_start(entry_box, False, False, 0)

        self.apply_btn = Gtk.Button(label="Apply")
        self.apply_btn.connect("clicked", self.on_apply)
        box.pack_start(self.apply_btn, False, False, 0)

        self.status = Gtk.Label(label="", xalign=0)
        self.status.set_line_wrap(True)
        box.pack_start(self.status, False, False, 0)

        self.connect("destroy", Gtk.main_quit)

    def on_apply(self, _btn):
        custom = self.custom_entry.get_text().strip()
        geom = custom if custom else self.combo.get_active_text()
        if not geom or not GEOM_RE.match(geom):
            self.status.set_text(
                "Enter a resolution as WIDTHxHEIGHT, e.g. 1920x1080."
            )
            return
        self.apply_btn.set_sensitive(False)
        self.status.set_text(
            f"Applying {geom} -- the desktop will restart in a few seconds..."
        )
        # Deliberately detached (start_new_session=True): the script
        # itself has to keep running to completion independent of this
        # app's own lifetime, not get SIGHUP'd along with everything
        # else in the dying session.
        subprocess.Popen(
            [VNC_SCRIPT, geom],
            start_new_session=True,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )
        # Real bug found and fixed here (2026-09-20), reported live by a
        # user: the restart never actually happened until this window
        # was closed by hand. Root cause -- `linuxdoors-gnome51-vnc`'s
        # `systemctl --user stop gnome-shell51-vnc.service` call is
        # SYNCHRONOUS (systemctl's own default behaviour: it blocks
        # until the unit actually reports inactive) -- and Mutter's own
        # shutdown sequence, as a real Wayland/XWayland compositor,
        # waits for its connected clients to disconnect before it
        # actually finishes exiting. This window is exactly such a
        # client (an XWayland app, this system's GTK3 has no native
        # Wayland backend at all -- see linuxdoors-vncviewer's own
        # notes on the same point), and it had no reason to ever
        # disconnect on its own: Gtk.main() just keeps running,
        # "Applying..." on screen forever, silently blocking the very
        # stop it just asked for. Since the whole point of Apply is
        # that this session is about to restart anyway, there is
        # nothing left for this window to do -- closing it immediately
        # (after a short pause so the status text is actually readable)
        # is what lets Mutter's shutdown -- and therefore the whole
        # restart -- actually complete.
        GLib.timeout_add(600, Gtk.main_quit)


def main():
    win = ResolutionWindow()
    win.show_all()
    Gtk.main()


if __name__ == "__main__":
    main()
