mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 08:00:09 +02:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80797c98b9 | ||
|
|
2fadbbe148 | ||
|
|
5ee07f99a8 | ||
|
|
d995b815f8 |
No files matched your search
+2
-1
@@ -185,6 +185,8 @@ KWin renders with OpenGL through zink on Turnip, Vulkan on the same GPU vrcompos
|
|||||||
|
|
||||||
The nested session also runs the system's XDG autostart entries, being a KDE session. Discover's update notifier started `plasma-discover --mode update` in it (520 to 620 MB resident and about 9% of a core, plus `flatpak-system-helper` and AppStream downloads), and IBus started a daemon, the kimpanel panel and its GTK extension that nothing can use: KWin hands text input to the one input method it starts (`ft-textinput`), and the session drops `QT_IM_MODULE`, `GTK_IM_MODULE` and `XMODIFIERS`. The session hides both for this desktop only, with `Hidden=true` copies in its own autostart folder. The geoclue demo agent stays: it's what answers apps' location requests to Geoclue outside GNOME, and it costs nothing while idle. Orca's entry only starts in GNOME-family desktops.
|
The nested session also runs the system's XDG autostart entries, being a KDE session. Discover's update notifier started `plasma-discover --mode update` in it (520 to 620 MB resident and about 9% of a core, plus `flatpak-system-helper` and AppStream downloads), and IBus started a daemon, the kimpanel panel and its GTK extension that nothing can use: KWin hands text input to the one input method it starts (`ft-textinput`), and the session drops `QT_IM_MODULE`, `GTK_IM_MODULE` and `XMODIFIERS`. The session hides both for this desktop only, with `Hidden=true` copies in its own autostart folder. The geoclue demo agent stays: it's what answers apps' location requests to Geoclue outside GNOME, and it costs nothing while idle. Orca's entry only starts in GNOME-family desktops.
|
||||||
|
|
||||||
|
Plasma 6.2.5 keeps each panel on a screen number (`lastScreen` in `plasma-org.kde.plasma.desktop-appletsrc`), and the numbers rank the enabled outputs by priority, so 0 is the primary screen. A panel whose number is past the screen count gets no view, and Plasma never moves it: the remap it runs at every start only moves a panel whose number has no desktop, and this desktop keeps a desktop for every output it has seen, spares included. So the taskbar was lost when the number of screens went down, and once it was found saved on a spare output, number 8 of a desktop with three screens ([#18](https://github.com/DeeJanuz/frametop/issues/18)). Before Plasma starts, the session runs `session/fix-panels.py`, which moves any panel numbered past the screen count, with its system tray's containment, to screen 0, keeping its widgets and settings. A panel stays put when screen 0 already has one on that edge, and comes back by itself if the screens do. The file is backed up to `<file>.ft-bak` first. Plasma's scripting can't do this while it runs (`panel.screen` is read-only in 6.2.5), so a lost taskbar comes back at the desktop's next start. `scripts/doctor.sh` and `scripts/report.sh` list the panels and their screens.
|
||||||
|
|
||||||
Remote desktop is a chain (krdp, then FreeRDP inside Xvnc) because nothing on SteamOS serves KWin over VNC directly. Kept connected all the time, it cost about a core with nobody watching: krdpserver 55 to 78% (it encodes H.264 in software with openh264: VA-API finds no driver for the Frame's GPU in the container), FreeRDP 16 to 27%, Xvnc 6 to 11%, and the bridge's layout check every 5 seconds another 4%. krdp creates its screencast session per RDP connection and drops it when the connection closes (`SessionController::onNewConnection` in krdp 6.7), so an idle krdpserver costs nothing and can stay up; only the RDP connection has to go. The bridge connects FreeRDP when a VNC client appears and disconnects 45 seconds after the last one leaves. Xvnc has no hook for its clients, so the bridge counts established connections to its port with `ss`, woken early by Xvnc's log output; looking with `ss` once a second cost about 0.9% of a core in bash, against about 0.1% this way. `Xvnc -inetd` from a systemd socket would start a server per connection and lose sharing between viewers. The layout check (`ft-layout remote-view`, which scans `/proc` for plasmashell and runs `kscreen-doctor -j`) now runs only while FreeRDP runs, and then only after `kwinoutputconfig.json` or `frametop-layout.json` changes, with one check a minute in case a change touched neither.
|
Remote desktop is a chain (krdp, then FreeRDP inside Xvnc) because nothing on SteamOS serves KWin over VNC directly. Kept connected all the time, it cost about a core with nobody watching: krdpserver 55 to 78% (it encodes H.264 in software with openh264: VA-API finds no driver for the Frame's GPU in the container), FreeRDP 16 to 27%, Xvnc 6 to 11%, and the bridge's layout check every 5 seconds another 4%. krdp creates its screencast session per RDP connection and drops it when the connection closes (`SessionController::onNewConnection` in krdp 6.7), so an idle krdpserver costs nothing and can stay up; only the RDP connection has to go. The bridge connects FreeRDP when a VNC client appears and disconnects 45 seconds after the last one leaves. Xvnc has no hook for its clients, so the bridge counts established connections to its port with `ss`, woken early by Xvnc's log output; looking with `ss` once a second cost about 0.9% of a core in bash, against about 0.1% this way. `Xvnc -inetd` from a systemd socket would start a server per connection and lose sharing between viewers. The layout check (`ft-layout remote-view`, which scans `/proc` for plasmashell and runs `kscreen-doctor -j`) now runs only while FreeRDP runs, and then only after `kwinoutputconfig.json` or `frametop-layout.json` changes, with one check a minute in case a change touched neither.
|
||||||
|
|
||||||
Program names stay within 15 characters, because Linux truncates process names there and the scripts find programs with `pgrep -x` and `pkill -x`. That's why the prefix is `ft-`.
|
Program names stay within 15 characters, because Linux truncates process names there and the scripts find programs with `pgrep -x` and `pkill -x`. That's why the prefix is `ft-`.
|
||||||
@@ -224,6 +226,5 @@ On the Frame, SteamVR is part of the OS image (`/opt/steamvr`, the `deckard-stea
|
|||||||
|
|
||||||
- A controller button that shows the screens during a game. Games own the controllers, so this needs SteamVR input actions for ft-screens.
|
- A controller button that shows the screens during a game. Games own the controllers, so this needs SteamVR input actions for ft-screens.
|
||||||
- Drawing KWin's cursor on the screens.
|
- Drawing KWin's cursor on the screens.
|
||||||
- Plasma can lose its panels when the number of screens goes down, because they're saved against a screen that no longer exists. Removing `plasma-org.kde.plasma.desktop-appletsrc` and `plasmashellrc` from `~/.config/frametop` brings the default panels back.
|
|
||||||
- Frame pacing and GPU cost with several busy screens haven't been measured.
|
- Frame pacing and GPU cost with several busy screens haven't been measured.
|
||||||
- Real standby on a stand, with rendering and tracking paused, not just the backlight off. SteamVR has no call for it, and its activity level follows the proximity sensor.
|
- Real standby on a stand, with rendering and tracking paused, not just the backlight off. SteamVR has no call for it, and its activity level follows the proximity sensor.
|
||||||
@@ -1409,6 +1409,7 @@ struct Press {
|
|||||||
double x = 0, y = 0; // ...and where on it, in buffer pixels
|
double x = 0, y = 0; // ...and where on it, in buffer pixels
|
||||||
double distance = 1; // from the laser's start to the last panel it met
|
double distance = 1; // from the laser's start to the last panel it met
|
||||||
long upAt = -1; // the pointer helper saw left come up: release it at this tick
|
long upAt = -1; // the pointer helper saw left come up: release it at this tick
|
||||||
|
int64_t idleSince = -1; // the pressing controller has held nothing since (ms, ReleaseStuck)
|
||||||
};
|
};
|
||||||
Press g_press;
|
Press g_press;
|
||||||
vr::VROverlayHandle_t g_catcher = vr::k_ulOverlayHandleInvalid;
|
vr::VROverlayHandle_t g_catcher = vr::k_ulOverlayHandleInvalid;
|
||||||
@@ -1445,6 +1446,47 @@ void ReleaseAway(uint32_t button, void (*handle)(const struct ft_event *, void *
|
|||||||
handle(&e, data);
|
handle(&e, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Whether a hand controller holds anything (a button down): 1 yes, 0 no, -1 unknown. SteamVR
|
||||||
|
// answers overlay apps only while a VR game runs (checked 2026-10-04); outside games it can't
|
||||||
|
// say. The Frame controller's axes have no types, so the buttons are all there is.
|
||||||
|
int ControllerHolds(vr::TrackedDeviceIndex_t dev) {
|
||||||
|
vr::VRControllerState_t st{};
|
||||||
|
if (!vr::VRSystem()->GetControllerState(dev, &st, sizeof st)) return -1;
|
||||||
|
return st.ulButtonPressed ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Releases SteamVR never sends. Pausing, or hiding the screen a button went down on, takes the
|
||||||
|
// laser off it mid-click (the pause gesture's second thumbstick click does that), and SteamVR's
|
||||||
|
// laser mouse then forgets the button: no release comes, the catcher stayed up, and the game
|
||||||
|
// never got its controllers back (2026-10-04). So a held button is released here when it has
|
||||||
|
// no visible screen to come up on, or when its hand controller has held nothing for kStuckMs
|
||||||
|
// (only known during VR games, where the lost release took the game's controllers).
|
||||||
|
// The pointer helper's virtual controller has its own word for that ("up").
|
||||||
|
constexpr int64_t kStuckMs = 1000;
|
||||||
|
void ReleaseStuck(void (*handle)(const struct ft_event *, void *), void *data) {
|
||||||
|
if (!g_press.buttons) {
|
||||||
|
g_press.idleSince = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const char *why = nullptr;
|
||||||
|
const auto it = g_screens.find(g_press.screen);
|
||||||
|
if (g_paused) why = "paused";
|
||||||
|
else if (g_press.screen >= 0 && (it == g_screens.end() || !it->second.visible)) why = "its screen hid";
|
||||||
|
else if (IsHandController(g_press.device)) {
|
||||||
|
const int64_t now = NowMs();
|
||||||
|
if (ControllerHolds(g_press.device) != 0) g_press.idleSince = -1;
|
||||||
|
else if (g_press.idleSince < 0) g_press.idleSince = now;
|
||||||
|
else if (now - g_press.idleSince >= kStuckMs) why = "the controller holds nothing";
|
||||||
|
}
|
||||||
|
if (!why) return;
|
||||||
|
std::printf("a held button can't come up on a screen (%s): released\n", why);
|
||||||
|
const vr::TrackedDeviceIndex_t dev = g_press.device;
|
||||||
|
for (uint32_t b = 0; b < 32; ++b)
|
||||||
|
if (g_press.buttons & (1u << b)) ReleaseAway(BTN_LEFT + b, handle, data);
|
||||||
|
g_press.buttons = 0, g_press.upAt = -1, g_press.idleSince = -1;
|
||||||
|
EndDragsBy(dev);
|
||||||
|
}
|
||||||
|
|
||||||
void ShowCatcher(bool on) {
|
void ShowCatcher(bool on) {
|
||||||
if (on == g_catcherShown || g_catcher == vr::k_ulOverlayHandleInvalid) return;
|
if (on == g_catcherShown || g_catcher == vr::k_ulOverlayHandleInvalid) return;
|
||||||
g_catcherShown = on;
|
g_catcherShown = on;
|
||||||
@@ -2232,6 +2274,7 @@ void ft_vr_poll(void (*handle)(const struct ft_event *, void *), void *data) {
|
|||||||
if (ev.eventType == vr::VREvent_MouseButtonUp)
|
if (ev.eventType == vr::VREvent_MouseButtonUp)
|
||||||
ReleaseAwayBy(ev.trackedDeviceIndex, ev.data.mouse.button, handle, data);
|
ReleaseAwayBy(ev.trackedDeviceIndex, ev.data.mouse.button, handle, data);
|
||||||
if (g_press.upAt >= 0 && g_tick >= g_press.upAt) ReleaseAway(BTN_LEFT, handle, data);
|
if (g_press.upAt >= 0 && g_tick >= g_press.upAt) ReleaseAway(BTN_LEFT, handle, data);
|
||||||
|
ReleaseStuck(handle, data);
|
||||||
RefreshChrome();
|
RefreshChrome();
|
||||||
while (vr::VRSystem()->PollNextEvent(&ev, sizeof ev)) {
|
while (vr::VRSystem()->PollNextEvent(&ev, sizeof ev)) {
|
||||||
if (ev.eventType == vr::VREvent_Quit) {
|
if (ev.eventType == vr::VREvent_Quit) {
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ check "distrobox" on_frame 'test -x ~/.local/bin/distrobox && ~/.local/bin/distr
|
|||||||
check "container $FRAME_BOX" on_frame "podman ps -a --filter name=^$FRAME_BOX\$ --format '{{.Image}} {{.Status}}' | grep ."
|
check "container $FRAME_BOX" on_frame "podman ps -a --filter name=^$FRAME_BOX\$ --format '{{.Image}} {{.Status}}' | grep ."
|
||||||
check "repo on the Frame" on_frame 'pwd'
|
check "repo on the Frame" on_frame 'pwd'
|
||||||
check "free space in ~" on_frame "df -h ~ | awk 'NR==2{print \$4\" free\"}'"
|
check "free space in ~" on_frame "df -h ~ | awk 'NR==2{print \$4\" free\"}'"
|
||||||
|
# A taskbar saved on a screen the desktop doesn't have is hidden; the desktop's next start
|
||||||
|
# moves it to the first screen (session/fix-panels.py).
|
||||||
|
check "taskbar on a screen" on_frame 'set -o pipefail; [ -f session/fix-panels.py ] || { echo "not checked (older checkout)"; exit 0; }
|
||||||
|
python3 session/fix-panels.py --check | paste -sd ";" | sed "s/;/; /g"'
|
||||||
|
|
||||||
echo "what Frametop needs from SteamOS:"
|
echo "what Frametop needs from SteamOS:"
|
||||||
if [ "$FRAME_LOCAL" = 1 ]; then
|
if [ "$FRAME_LOCAL" = 1 ]; then
|
||||||
|
|||||||
@@ -42,6 +42,25 @@ for i, s in enumerate(d.get("screens", []), 1):
|
|||||||
print("visibility:", d.get("visibility"))
|
print("visibility:", d.get("visibility"))
|
||||||
PY
|
PY
|
||||||
|
|
||||||
|
section "Plasma panels and outputs"
|
||||||
|
python3 "$repo/session/fix-panels.py" --check 2>&1 || true
|
||||||
|
python3 - "$repo" <<'PY' 2>&1 || true
|
||||||
|
import json, subprocess, sys
|
||||||
|
sys.path.insert(0, sys.argv[1] + "/layout")
|
||||||
|
import ft_layout
|
||||||
|
env = ft_layout.nested_env()
|
||||||
|
if not env:
|
||||||
|
sys.exit(print("desktop not running: no live outputs or panels"))
|
||||||
|
outs = json.loads(subprocess.run(["kscreen-doctor", "-j"], capture_output=True, text=True, env=env, timeout=10).stdout or "{}")
|
||||||
|
for o in outs.get("outputs", []):
|
||||||
|
size = o.get("size") or {}
|
||||||
|
print(f"output {o.get('name')}: enabled={o.get('enabled')} priority={o.get('priority')} {size.get('width')}x{size.get('height')}")
|
||||||
|
js = "print(JSON.stringify(panels().map(p => ({id: p.id, screen: p.screen, location: p.location}))))"
|
||||||
|
r = subprocess.run(["qdbus6", "org.kde.plasmashell", "/PlasmaShell", "org.kde.PlasmaShell.evaluateScript", js],
|
||||||
|
capture_output=True, text=True, env=env, timeout=10)
|
||||||
|
print("live panels:", (r.stdout or r.stderr).strip())
|
||||||
|
PY
|
||||||
|
|
||||||
section "Input relay (last 60 lines)"
|
section "Input relay (last 60 lines)"
|
||||||
journalctl --user -u frametop-input-relay -n 60 --no-pager -o short 2>/dev/null
|
journalctl --user -u frametop-input-relay -n 60 --no-pager -o short 2>/dev/null
|
||||||
section "Pointer helper (last 60 lines)"
|
section "Pointer helper (last 60 lines)"
|
||||||
|
|||||||
Executable
+143
@@ -0,0 +1,143 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Bring back a taskbar that Plasma saved against a screen this desktop doesn't have.
|
||||||
|
|
||||||
|
Plasma 6.2.5 ties each panel to a screen number (lastScreen in its containment), and the
|
||||||
|
numbers rank the enabled outputs by priority: 0 is the primary screen. A panel whose
|
||||||
|
number is past the screen count gets no view and stays hidden; Plasma never moves it,
|
||||||
|
even on a later start (sanitizeScreenLayout() only remaps a panel whose number has no
|
||||||
|
desktop, and the Frametop desktop keeps a desktop for every spare output it has seen).
|
||||||
|
So a taskbar saved on a spare output (issue #18), or on a screen that a smaller layout
|
||||||
|
dropped, was lost until the config was deleted.
|
||||||
|
|
||||||
|
The session runs this before Plasma starts, so nothing races Plasma for the file. Each
|
||||||
|
panel numbered at or past the screen count moves to screen 0, with its system tray's
|
||||||
|
containment, and keeps its widgets and settings. A panel is left where it is when screen
|
||||||
|
0 already has a panel on that edge: it comes back by itself if the screens do. The file
|
||||||
|
is backed up once per repair (<file>.ft-bak), and the changes go through kwriteconfig6.
|
||||||
|
|
||||||
|
fix-panels.py [--screens N] [--file APPLETSRC] [--check]
|
||||||
|
--screens the desktop's screen count (default: the configured layout's)
|
||||||
|
--file default: $XDG_CONFIG_HOME/plasma-org.kde.plasma.desktop-appletsrc, with
|
||||||
|
XDG_CONFIG_HOME defaulting to the Frametop desktop's ~/.config/frametop
|
||||||
|
--check change nothing; list the panels, and exit 1 if one is lost
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
EDGES = {3: "top", 4: "bottom", 5: "left", 6: "right"} # Plasma::Types::Location
|
||||||
|
SYSTRAY = "org.kde.plasma.private.systemtray"
|
||||||
|
GROUP = re.compile(r"\[([^\]]*)\]")
|
||||||
|
|
||||||
|
|
||||||
|
def parse(text):
|
||||||
|
"""KConfig text -> {(group, subgroup, ...): {key: value}}."""
|
||||||
|
groups, cur = {}, None
|
||||||
|
for line in text.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith("["):
|
||||||
|
cur = tuple(GROUP.findall(line))
|
||||||
|
groups.setdefault(cur, {})
|
||||||
|
elif cur is not None and "=" in line and not line.startswith("#"):
|
||||||
|
k, v = line.split("=", 1)
|
||||||
|
groups[cur][re.sub(r"\[\$.*\]$", "", k.strip())] = v.strip()
|
||||||
|
return groups
|
||||||
|
|
||||||
|
|
||||||
|
def number(v, default=-1):
|
||||||
|
try:
|
||||||
|
return int(v)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def panels(groups):
|
||||||
|
"""The panels, by containment id: location, lastScreen, and the ids of their system
|
||||||
|
trays' own containments (which sit on the same edge and screen as the panel)."""
|
||||||
|
trays, found = {}, {}
|
||||||
|
for g, keys in groups.items():
|
||||||
|
if len(g) == 5 and g[0] == "Containments" and g[2] == "Applets" and g[4] == "Configuration":
|
||||||
|
tray = keys.get("SystrayContainmentId")
|
||||||
|
if tray:
|
||||||
|
trays.setdefault(g[1], []).append(tray)
|
||||||
|
owned = {t for ts in trays.values() for t in ts}
|
||||||
|
for g, keys in groups.items():
|
||||||
|
if len(g) != 2 or g[0] != "Containments" or number(g[1]) <= 0 or g[1] in owned:
|
||||||
|
continue
|
||||||
|
loc = number(keys.get("location"), 0)
|
||||||
|
if loc in EDGES and keys.get("plugin") != SYSTRAY:
|
||||||
|
found[g[1]] = {"location": loc, "screen": number(keys.get("lastScreen")),
|
||||||
|
"plugin": keys.get("plugin", "?"), "trays": trays.get(g[1], [])}
|
||||||
|
return dict(sorted(found.items(), key=lambda kv: number(kv[0])))
|
||||||
|
|
||||||
|
|
||||||
|
def plan(groups, screens):
|
||||||
|
"""(moves, kept): moves are (panel id, from screen, containment ids to put on screen 0);
|
||||||
|
kept are (panel id, from screen, why) for lost panels left alone."""
|
||||||
|
found = panels(groups)
|
||||||
|
taken = {(p["screen"], p["location"]) for p in found.values() if 0 <= p["screen"] < screens}
|
||||||
|
moves, kept = [], []
|
||||||
|
for pid, p in found.items():
|
||||||
|
if p["screen"] < screens:
|
||||||
|
continue # on a screen this desktop has (Plasma puts -1 on the first one itself)
|
||||||
|
if (0, p["location"]) in taken:
|
||||||
|
kept.append((pid, p["screen"], f"the first screen already has a {EDGES[p['location']]} panel"))
|
||||||
|
continue
|
||||||
|
taken.add((0, p["location"]))
|
||||||
|
moves.append((pid, p["screen"], [pid, *p["trays"]]))
|
||||||
|
return moves, kept
|
||||||
|
|
||||||
|
|
||||||
|
def default_screens():
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "layout"))
|
||||||
|
import ft_layout
|
||||||
|
return ft_layout.screen_count()
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
|
||||||
|
ap.add_argument("--screens", type=int)
|
||||||
|
ap.add_argument("--file")
|
||||||
|
ap.add_argument("--check", action="store_true")
|
||||||
|
a = ap.parse_args(argv)
|
||||||
|
config = os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config/frametop")
|
||||||
|
path = a.file or os.path.join(config, "plasma-org.kde.plasma.desktop-appletsrc")
|
||||||
|
screens = a.screens if a.screens else default_screens()
|
||||||
|
try:
|
||||||
|
with open(path) as f:
|
||||||
|
groups = parse(f.read())
|
||||||
|
except FileNotFoundError:
|
||||||
|
if a.check:
|
||||||
|
print("no Plasma config yet (Plasma makes the default taskbar)")
|
||||||
|
return 0
|
||||||
|
moves, kept = plan(groups, screens)
|
||||||
|
|
||||||
|
if a.check:
|
||||||
|
found = panels(groups)
|
||||||
|
for pid, p in found.items():
|
||||||
|
lost = " (lost: no such screen)" if p["screen"] >= screens else ""
|
||||||
|
print(f"panel {pid}: screen {p['screen']}, {EDGES[p['location']]}{lost}")
|
||||||
|
if not found:
|
||||||
|
print("no panels")
|
||||||
|
print(f"{screens} screen(s)")
|
||||||
|
return 1 if moves or kept else 0
|
||||||
|
|
||||||
|
if moves:
|
||||||
|
shutil.copy2(path, path + ".ft-bak")
|
||||||
|
for pid, was, ids in moves:
|
||||||
|
for cid in ids:
|
||||||
|
subprocess.run(["kwriteconfig6", "--file", os.path.abspath(path), "--group", "Containments",
|
||||||
|
"--group", cid, "--key", "lastScreen", "0"], check=True)
|
||||||
|
print(f"frametop: panel {pid} was saved on screen {was}, which this desktop doesn't have "
|
||||||
|
f"({screens} screen(s)); moved it to the first screen (backup: {path}.ft-bak)", file=sys.stderr)
|
||||||
|
for pid, was, why in kept:
|
||||||
|
print(f"frametop: panel {pid} is saved on screen {was}, which this desktop doesn't have; "
|
||||||
|
f"left there: {why}", file=sys.stderr)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv[1:]))
|
||||||
@@ -272,4 +272,9 @@ fi
|
|||||||
# with both, apps would open twice.
|
# with both, apps would open twice.
|
||||||
kwriteconfig6 --file "$XDG_CONFIG_HOME/ksmserverrc" --group General --key loginMode emptySession
|
kwriteconfig6 --file "$XDG_CONFIG_HOME/ksmserverrc" --group General --key loginMode emptySession
|
||||||
|
|
||||||
|
# Plasma keeps a panel on a screen number, and never moves one whose screen this desktop
|
||||||
|
# doesn't have, like a spare output or a screen a smaller layout dropped. Put such a panel
|
||||||
|
# back on the first (primary) screen before Plasma reads the file (session/fix-panels.py).
|
||||||
|
python3 "$here/fix-panels.py" --screens "$screens" || true
|
||||||
|
|
||||||
dbus-run-session startplasma-wayland
|
dbus-run-session startplasma-wayland
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Tests for session/fix-panels.py: issue #18's config (the taskbar saved on spare output 8
|
||||||
|
of a 3-screen desktop), panels that are fine, an edge that's taken, and a real run of
|
||||||
|
kwriteconfig6 on a copy. Nothing here touches the running desktop or its config.
|
||||||
|
|
||||||
|
python3 session/tests/test_fix_panels.py
|
||||||
|
"""
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
SCRIPT = os.path.join(HERE, "..", "fix-panels.py")
|
||||||
|
spec = importlib.util.spec_from_file_location("fix_panels", SCRIPT)
|
||||||
|
fp = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(fp)
|
||||||
|
|
||||||
|
|
||||||
|
def desktop(cid, screen):
|
||||||
|
return f"""[Containments][{cid}]
|
||||||
|
activityId=7e1d1a3c-0000-4000-8000-000000000000
|
||||||
|
formfactor=0
|
||||||
|
immutability=1
|
||||||
|
lastScreen={screen}
|
||||||
|
location=0
|
||||||
|
plugin=org.kde.plasma.folder
|
||||||
|
wallpaperplugin=org.kde.image
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def panel(cid, screen, location=4, tray=None):
|
||||||
|
text = f"""[Containments][{cid}]
|
||||||
|
activityId=
|
||||||
|
formfactor=2
|
||||||
|
immutability=1
|
||||||
|
lastScreen={screen}
|
||||||
|
location={location}
|
||||||
|
plugin=org.kde.panel
|
||||||
|
wallpaperplugin=org.kde.image
|
||||||
|
|
||||||
|
[Containments][{cid}][Applets][{cid}1]
|
||||||
|
immutability=1
|
||||||
|
plugin=org.kde.plasma.kickoff
|
||||||
|
"""
|
||||||
|
if tray:
|
||||||
|
text += f"""
|
||||||
|
[Containments][{cid}][Applets][{cid}2]
|
||||||
|
immutability=1
|
||||||
|
plugin=org.kde.plasma.systemtray
|
||||||
|
|
||||||
|
[Containments][{cid}][Applets][{cid}2][Configuration]
|
||||||
|
PreloadWeight=90
|
||||||
|
SystrayContainmentId={tray}
|
||||||
|
|
||||||
|
[Containments][{tray}]
|
||||||
|
activityId=
|
||||||
|
formfactor=2
|
||||||
|
immutability=1
|
||||||
|
lastScreen={screen}
|
||||||
|
location={location}
|
||||||
|
plugin=org.kde.plasma.private.systemtray
|
||||||
|
popupHeight=432
|
||||||
|
"""
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
ISSUE_18 = "\n".join([desktop(1, 0), desktop(18, 1), desktop(19, 2)] + [desktop(20 + i, 3 + i) for i in range(8)]
|
||||||
|
+ [panel(10, 8, tray=11), "[ScreenMapping]\nitemsOnDisabledScreens=\n"])
|
||||||
|
|
||||||
|
|
||||||
|
class Plan(unittest.TestCase):
|
||||||
|
def test_issue_18(self):
|
||||||
|
moves, kept = fp.plan(fp.parse(ISSUE_18), 3)
|
||||||
|
self.assertEqual(moves, [("10", 8, ["10", "11"])])
|
||||||
|
self.assertEqual(kept, [])
|
||||||
|
|
||||||
|
def test_panel_on_a_screen_stays(self):
|
||||||
|
for screen in (0, 2, -1):
|
||||||
|
moves, kept = fp.plan(fp.parse(desktop(1, 0) + panel(2, screen, tray=8)), 3)
|
||||||
|
self.assertEqual((moves, kept), ([], []), screen)
|
||||||
|
|
||||||
|
def test_tray_is_not_a_panel(self):
|
||||||
|
found = fp.panels(fp.parse(panel(2, 0, tray=8)))
|
||||||
|
self.assertEqual(list(found), ["2"])
|
||||||
|
self.assertEqual(found["2"]["trays"], ["8"])
|
||||||
|
|
||||||
|
def test_desktops_never_move(self):
|
||||||
|
moves, _ = fp.plan(fp.parse(ISSUE_18), 1)
|
||||||
|
self.assertEqual([m[0] for m in moves], ["10"])
|
||||||
|
|
||||||
|
def test_edge_taken_on_first_screen(self):
|
||||||
|
text = panel(2, 0, tray=8) + panel(10, 8, tray=11)
|
||||||
|
moves, kept = fp.plan(fp.parse(text), 3)
|
||||||
|
self.assertEqual(moves, [])
|
||||||
|
self.assertEqual([k[:2] for k in kept], [("10", 8)])
|
||||||
|
|
||||||
|
def test_other_edge_is_free(self):
|
||||||
|
moves, kept = fp.plan(fp.parse(panel(2, 0) + panel(10, 4, location=3)), 3)
|
||||||
|
self.assertEqual(moves, [("10", 4, ["10"])])
|
||||||
|
self.assertEqual(kept, [])
|
||||||
|
|
||||||
|
def test_two_lost_on_one_edge(self):
|
||||||
|
moves, kept = fp.plan(fp.parse(panel(10, 5) + panel(12, 8)), 3)
|
||||||
|
self.assertEqual([m[0] for m in moves], ["10"])
|
||||||
|
self.assertEqual([k[0] for k in kept], ["12"])
|
||||||
|
|
||||||
|
def test_screen_count_went_down(self):
|
||||||
|
moves, _ = fp.plan(fp.parse(panel(2, 1, location=3)), 1)
|
||||||
|
self.assertEqual(moves, [("2", 1, ["2"])])
|
||||||
|
|
||||||
|
|
||||||
|
class Run(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.dir = tempfile.TemporaryDirectory()
|
||||||
|
self.path = os.path.join(self.dir.name, "plasma-org.kde.plasma.desktop-appletsrc")
|
||||||
|
with open(self.path, "w") as f:
|
||||||
|
f.write(ISSUE_18)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.dir.cleanup()
|
||||||
|
|
||||||
|
def run_script(self, *args):
|
||||||
|
return subprocess.run([sys.executable, SCRIPT, "--file", self.path, "--screens", "3", *args],
|
||||||
|
capture_output=True, text=True)
|
||||||
|
|
||||||
|
def test_check_reports_and_changes_nothing(self):
|
||||||
|
r = self.run_script("--check")
|
||||||
|
self.assertEqual(r.returncode, 1)
|
||||||
|
self.assertIn("panel 10: screen 8, bottom (lost", r.stdout)
|
||||||
|
with open(self.path) as f:
|
||||||
|
self.assertEqual(f.read(), ISSUE_18)
|
||||||
|
|
||||||
|
def test_repair(self):
|
||||||
|
r = self.run_script()
|
||||||
|
self.assertEqual(r.returncode, 0, r.stderr)
|
||||||
|
self.assertIn("moved it to the first screen", r.stderr)
|
||||||
|
with open(self.path) as f:
|
||||||
|
groups = fp.parse(f.read())
|
||||||
|
self.assertEqual(groups[("Containments", "10")]["lastScreen"], "0")
|
||||||
|
self.assertEqual(groups[("Containments", "11")]["lastScreen"], "0")
|
||||||
|
self.assertEqual(groups[("Containments", "10", "Applets", "101")]["plugin"], "org.kde.plasma.kickoff")
|
||||||
|
self.assertEqual(groups[("Containments", "25")]["lastScreen"], "8") # a spare's desktop
|
||||||
|
with open(self.path + ".ft-bak") as f:
|
||||||
|
self.assertEqual(f.read(), ISSUE_18)
|
||||||
|
# A second run finds nothing to do and leaves the backup alone.
|
||||||
|
os.remove(self.path + ".ft-bak")
|
||||||
|
r = self.run_script()
|
||||||
|
self.assertEqual((r.returncode, r.stderr), (0, ""))
|
||||||
|
self.assertFalse(os.path.exists(self.path + ".ft-bak"))
|
||||||
|
self.assertEqual(self.run_script("--check").returncode, 0)
|
||||||
|
|
||||||
|
def test_no_config_yet(self):
|
||||||
|
os.remove(self.path)
|
||||||
|
self.assertEqual(self.run_script().returncode, 0)
|
||||||
|
self.assertEqual(self.run_script("--check").returncode, 0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in new issue
Block a user