Merge branch fix/0x1f6-pr35 (our fixes for PR #35) into experimental

This commit is contained in:
DeeJanuz committed 2026-10-05 16:56:34 -06:00
commit b21dfd1bb0
3 files changed
+32 -12

No files matched your search

+2 -1
View File
@@ -85,12 +85,13 @@ In a game, Frametop's panels work like SteamVR's own floating windows: point a c
### The KWin script
The KWin side is a script (`float/frametop-float.js`), not a C++ effect, because a script keeps working across KWin updates and an effect would have to match the host's exact KWin build. KWin scripts can call D-Bus but can't serve it, so ft-floatd's commands come back through a long poll: the script calls `NextCommand`, which answers when a command is ready, or empty after 20 seconds, under KWin's 25-second D-Bus timeout. A few things about KWin's script engine:
The KWin side is a script (`float/frametop-float.js`), not a C++ effect, because a script keeps working across KWin updates and an effect would have to match the host's exact KWin build. KWin scripts can call D-Bus but can't serve it, so ft-floatd's commands come back through a long poll: the script calls `NextCommand`, which answers when a command is ready, or empty after 20 seconds, under KWin's 25-second D-Bus timeout. If no reply comes within 30 seconds, a watchdog calls again, and waits twice as long each time until a reply comes (up to 5 minutes). The 30 seconds have to stay above KWin's timeout, or a late reply would start a second poll. A few things about KWin's script engine:
- `windowAdded` reports popups as windows of their own (`popupWindow` true, `transient` true) with their geometry.
- Setting `frameGeometry` applies asynchronously: the app has to answer the new size first.
- A script can't read a window's maximize mode, so the script counts a window as maximized when it fills its output's maximize area.
- `globalThis` isn't defined. `print` goes to the journal unless `QT_FORCE_STDERR_LOGGING=1`.
- `callDBus` never calls back when a call fails (an error reply, the name gone from the bus, or the 25-second timeout). KWin only logs `Received D-Bus message is error`, so a script that waits for the callback waits forever.
The title bar's float button is Frametop's own window decoration (`decoration/`), written in QML for KWin's Aurorae engine, which loads it without compiling. A C++ fork of Breeze would have to match SteamOS's exact KDecoration build. A decoration can only make the window requests KWin offers it, so the button toggles keep-below, which has no visible effect on a window alone on its own output, and the script treats keep-below as the floating flag.
+1 -1
View File
@@ -85,7 +85,7 @@ ft-screens creates a `screen` for each toplevel in the order they appear, and in
runs commands ◀─ long poll ─ spare outputs (kscreen-doctor) ◀─ @frametop_float ─ lasers, catcher
```
- **KWin script `frametop-float`** (`float/frametop-float.js`). ft-floatd loads it into the desktop's KWin over D-Bus (`org.kde.kwin.Scripting`). A script keeps working across KWin updates. A C++ effect would have to match the host's exact KWin build, and the build container is Fedora, not SteamOS. The script watches windows (`windowAdded`/`windowRemoved`, `frameGeometryChanged`, `outputChanged`, `interactiveMoveResizeStarted`/`Finished`, `fullScreenChanged`, `maximizedChanged`, `minimizedChanged`, `keepBelowChanged`, `windowActivated`) and the outputs (`screensChanged`). It runs commands: move a window to an output, set its geometry, put it on all virtual desktops, and restore it. It adds "Float in VR" ("Back to Desktop" on a floating window) to the window menu (`registerUserActionsMenu`). It registers no shortcut: the float key belongs to the input relay. KWin scripts can call D-Bus but can't serve it, so commands come back through a long poll. The script calls ft-floatd's `NextCommand`, which answers when a command is ready, and then the script calls it again. It also keeps KWin's placement memory from moving windows (see design.md).
- **KWin script `frametop-float`** (`float/frametop-float.js`). ft-floatd loads it into the desktop's KWin over D-Bus (`org.kde.kwin.Scripting`). A script keeps working across KWin updates. A C++ effect would have to match the host's exact KWin build, and the build container is Fedora, not SteamOS. The script watches windows (`windowAdded`/`windowRemoved`, `frameGeometryChanged`, `outputChanged`, `interactiveMoveResizeStarted`/`Finished`, `fullScreenChanged`, `maximizedChanged`, `minimizedChanged`, `keepBelowChanged`, `windowActivated`) and the outputs (`screensChanged`). It runs commands: move a window to an output, set its geometry, put it on all virtual desktops, and restore it. It adds "Float in VR" ("Back to Desktop" on a floating window) to the window menu (`registerUserActionsMenu`). It registers no shortcut: the float key belongs to the input relay. KWin scripts can call D-Bus but can't serve it, so commands come back through a long poll. The script calls ft-floatd's `NextCommand`, which answers when a command is ready, and then the script calls it again, or after 30 seconds without an answer. It also keeps KWin's placement memory from moving windows (see design.md).
- **ft-floatd** (`float/ft-floatd`, Python). The host has dbus-python and PyGObject. It runs inside the desktop's Plasma session, started from its autostart. It owns `org.frametop.Float` on the session's private bus, and it keeps the table of which window is on which output and panel. It enables and disables spare outputs and sets their scale and position with `kscreen-doctor`, and their size through ft-screens. It tells ft-screens where each floating window goes and tells the script which window goes where. It launches apps floating, opens profiles' apps, and remembers each app's placement and scale, keyed by desktop file name. Commands come in on `@frametop_float`, from `ft-float`, the input relay, and ft-screens.
- **ft-screens.** A spare output's panel is a floating window's. Floating panels get the same bar, curve, roll, resize tab, and wrist and head pins as screens, plus dock and close buttons left of the bar. Other parts: the catcher, popup and dialog overlays, and carrying a panel during a KWin move. `MAX_SCREENS` (screens and spares together) is 24. Commands arrive on `@ft_screens`. Events go out to `@frametop_float` from an unbound socket, the same way ft-screens talks to the input relay.
- **Session script.** Adds `FLOAT_SLOTS` to KWin's output count, starts ft-floatd from the desktop's autostart, installs Frametop's window decoration and chooses it in the session's `kwinrc`, and writes the Launch as Standalone copies of the apps' desktop files.
+29 -10
View File
@@ -411,27 +411,46 @@ function run(c) {
}
}
// The long poll has one failure mode: a reply that never comes (ft-floatd dying mid-call,
// a D-Bus hiccup) leaves polling true forever and the script stops hearing commands until
// KWin reloads it. A watchdog re-arms the poll if the reply is late by more than
// ft-floatd's own timeout (POLL_SECONDS in ft_floatd.py) plus slack.
const POLL_SECONDS = 20;
// The long poll has one failure mode: a reply that never comes. KWin's callDBus never calls
// back when a call fails (an error reply, ft-floatd gone, or its 25 s D-Bus timeout, which
// ft-floatd blocked that long hits); it only logs "Received D-Bus message is error". Then
// polling stays true and the script stops hearing commands until KWin reloads it. A watchdog
// re-arms the poll. Its period must stay above that 25 s timeout, whatever ft-floatd's
// POLL_SECONDS is: by then the call it gives up on has ended. The serial is for a reply that
// still comes later (KWin stalled with the reply queued): it runs its commands (ft-floatd
// sends each one once) but doesn't poll again. Two polls would answer each other for good,
// since ft-floatd answers a waiting poll empty when the next one comes.
// When the watchdog fires again and again, ft-floatd is gone, and only starting it again
// helps (it reloads the script then). Each failed call is a line in KWin's log, so the
// script says so once and waits twice as long each time, up to 5 minutes. A reply starts
// over at 30 s, so a lost reply is still polled again after 30 s.
const POLL_WAIT = 30000, POLL_WAIT_MAX = 300000;
const pollWatchdog = new QTimer();
pollWatchdog.singleShot = true;
pollWatchdog.interval = (POLL_SECONDS + 10) * 1000;
pollWatchdog.interval = POLL_WAIT;
let pollLost = false; // the watchdog fired, and no reply since
pollWatchdog.timeout.connect(() => {
print("frametop-float: NextCommand didn't answer, polling again");
if (!pollLost) print("frametop-float: NextCommand didn't answer, polling again");
pollLost = true;
pollWatchdog.interval = Math.min(pollWatchdog.interval * 2, POLL_WAIT_MAX);
polling = false;
poll();
});
let pollSerial = 0;
function poll() {
if (polling) return;
polling = true;
const serial = ++pollSerial;
pollWatchdog.start();
callDBus(SERVICE, PATH, IFACE, "NextCommand", reply => {
pollWatchdog.stop();
polling = false;
const current = serial === pollSerial; // not a call the watchdog gave up on
if (current) {
pollWatchdog.stop();
pollWatchdog.interval = POLL_WAIT;
pollLost = false;
polling = false;
}
if (reply) {
try {
JSON.parse(reply).forEach(run);
@@ -439,7 +458,7 @@ function poll() {
print("frametop-float: bad command " + reply + ": " + e);
}
}
poll();
if (current) poll();
});
}