From a699540d95580a45299954f6b7ab2ac0966c1e75 Mon Sep 17 00:00:00 2001 From: DeeJanuz <45082401+DeeJanuz@users.noreply.github.com> Date: Wed, 30 Sep 2026 22:37:50 -0600 Subject: [PATCH] Profiles: fixes from trying them on the live desktop - KWin 6.2's scripts have no maximize mode: a window counts as maximized when it fills its output's maximize area. - A late event for a window that just closed brought it back into ft-floatd's table, so a profile "found" it open, floated a window that no longer existed, and held a slot. The script doesn't report deleted windows, and ft-floatd ignores events for ids it has seen close. - Docking a window that came from a screen that's hidden now puts it on the first screen that shows instead. Co-Authored-By: Claude Opus 5.5 --- docs/profiles.md | 2 +- float/frametop-float.js | 11 ++++++++++- float/ft_floatd.py | 17 ++++++++++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/docs/profiles.md b/docs/profiles.md index 8e40256..b243288 100644 --- a/docs/profiles.md +++ b/docs/profiles.md @@ -1,6 +1,6 @@ # Profiles (plan) -Status: design settled with the user on 2026-09-30, and built the same day on the branch `profiles`, which builds on `screen-hide` (screens hidden one at a time). Tested offline: saving, renaming, and deleting profiles with their launcher entries, against a stand-in ft-floatd. Still to try on the desktop: capturing and opening real apps, and starting the desktop in a profile. +Status: design settled with the user on 2026-09-30, and built the same day on the branch `profiles`, which builds on `screen-hide` (screens hidden one at a time). Tried on the live desktop the same day (headset off). A profile with a maximized Dolphin on screen 1, a floating Konsole, and screen 3 hidden saved correctly. Opened from nothing, it launched both into place. Opened over moved windows, it moved them back without launching anything. `ft-layout start` with `FT_PROFILE` and `ft-layout open` worked as well. Not yet tried: starting the desktop in a profile from its launcher entry or `default_profile` (needs a desktop restart), and the relay's `profile:NAME` action (the running relay is another branch's). A profile is a named layout that also opens apps. It holds: diff --git a/float/frametop-float.js b/float/frametop-float.js index 76b84bb..e15c05b 100644 --- a/float/frametop-float.js +++ b/float/frametop-float.js @@ -48,11 +48,20 @@ function info(w) { frame: rect(w.frameGeometry), client: rect(w.clientGeometry), popup: w.popupWindow, transient: w.transient, parent: w.transientFor ? String(w.transientFor.internalId) : "", normal: w.normalWindow, dialog: w.dialog, fullScreen: w.fullScreen, minimized: w.minimized, - onAllDesktops: w.onAllDesktops, maximized: w.maximizeMode === 3 + onAllDesktops: w.onAllDesktops, maximized: isMaximized(w) }; } +// KWin 6.2's scripts have no maximize mode to read: a window is maximized when it fills its +// output's maximize area. +function isMaximized(w) { + if (!w.normalWindow || !w.output) return false; + const a = workspace.clientArea(KWin.MaximizeArea, w), g = w.frameGeometry; + return g.x === a.x && g.y === a.y && g.width === a.width && g.height === a.height; +} + function report(type, w) { + if (w.deleted) return; // a window on its way out still changes output and size const ev = info(w); ev.ev = type; send(ev); diff --git a/float/ft_floatd.py b/float/ft_floatd.py index 554f41e..348da45 100644 --- a/float/ft_floatd.py +++ b/float/ft_floatd.py @@ -280,6 +280,7 @@ class Daemon: self.slots = [Slot(k, args.screens) for k in range(args.slots)] self.floats = {} # window id -> Float self.windows = {} # window id -> last info from the script + self.gone = set() # ids of windows that closed (KWin's ids aren't reused) self.pending = [] # commands for the script self.waiter = None # (reply callback, timeout source) while the script waits self.screens = Screens(args.control) @@ -354,7 +355,10 @@ class Daemon: # The script reports every window after "config"; spares nothing floats on are off. GLib.timeout_add(1500, self.disable_unused) return + if wid in self.gone: + return # an event that came after the window closed (it would bring it back) if kind == "removed": + self.gone.add(wid) self.windows.pop(wid, None) if wid in self.floats: log(f"{wid[:9]} closed") @@ -639,7 +643,18 @@ class Daemon: saved = f.saved or {"output": "WL-0", "frame": dict(f.frame or {"x": 100, "y": 100, "w": 800, "h": 600}), "onAllDesktops": False} fr = frame or saved["frame"] - output = output or saved["output"] + if not output: + output = saved["output"] + m = re.match(r"WL-(\d+)$", output) + hidden = self.concealed() + if m and int(m.group(1)) + 1 in hidden: + # It came from a screen that's hidden now: onto the first one that shows. + shown = [i for i in range(self.screens_n) if i + 1 not in hidden] + if shown: + x0, y0, ow, oh = output_rects().get(f"WL-{shown[0]}", (0, 0, 0, 0)) + output = f"WL-{shown[0]}" + w, h = min(fr["w"], ow or fr["w"]), min(fr["h"], oh or fr["h"]) + fr = {"x": x0 + max(0, (ow - w) / 2), "y": y0 + max(0, (oh - h) / 2), "w": w, "h": h} log(f"{f.id[:9]} back to {output}") self.command(cmd="place", id=f.id, output=output, x=fr["x"], y=fr["y"], w=fr["w"], h=fr["h"], onAllDesktops=bool(saved.get("onAllDesktops")), maximized=maximized)