fix-panels: the backup keeps the config as it was before the first repair

Every repair rewrote <file>.ft-bak, so the backup held the state of the
previous run, not the original: repair the config twice (the desktop saves
the panel on the lost screen again, with different widgets) and the copy of
the pristine config was gone. The backup is now written once, before the
first repair; later runs leave it alone. The docstring says what it now
really does.

A new test proves it against a kwriteconfig6 stub, so it runs without
Plasma's tools: with the old code it fails (the backup holds the drifted
state), with the new code the pre-first-repair original survives.

To see it on the Frame: session/tests/test_fix_panels.py (all 12, including
the real-kwriteconfig6 test_repair).
This commit is contained in:
0x1f6 committed 2026-10-05 23:24:59 +02:00
1 parent 276c1409e8
commit b61b4d6c6a
2 files changed
+55 -2

No files matched your search

+5 -2
View File
@@ -13,7 +13,8 @@ The session runs this before Plasma starts, so nothing races Plasma for the file
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.
is backed up before the first repair (<file>.ft-bak, and only then: a later repair never
overwrites that original), 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)
@@ -125,7 +126,9 @@ def main(argv):
print(f"{screens} screen(s)")
return 1 if moves or kept else 0
if moves:
if moves and not os.path.exists(path + ".ft-bak"):
# The backup holds the config as it was before the first repair, so a later run
# can't overwrite it with an already-repaired state.
shutil.copy2(path, path + ".ft-bak")
for pid, was, ids in moves:
for cid in ids:
+50
View File
@@ -157,6 +157,56 @@ class Run(unittest.TestCase):
self.assertEqual(self.run_script().returncode, 0)
self.assertEqual(self.run_script("--check").returncode, 0)
def test_backup_keeps_the_original(self):
"""The backup is written once, before the first repair: a later run that repairs
again never overwrites it with an already-repaired state. Runs against a
kwriteconfig6 stub, so it works without Plasma's tools."""
stubdir = os.path.join(self.dir.name, "bin")
os.mkdir(stubdir)
stub = os.path.join(stubdir, "kwriteconfig6")
with open(stub, "w") as f:
f.write("""#!/bin/sh
# Only what fix-panels.py uses: --file F --group Containments --group CID --key lastScreen 0
file=; cid=; val=
while [ $# -gt 0 ]; do
case $1 in
--file) file=$2; shift 2;;
--group) cid=$2; shift 2;;
--key) key=$2; shift 2;;
*) val=$1; shift;;
esac
done
awk -v cid="$cid" -v v="$val" '
$0 == "[Containments][" cid "]" { inblk = 1 }
/^\\[Containments\\]\\[[0-9]+\\]$/ && $0 != "[Containments][" cid "]" { inblk = 0 }
inblk && /^lastScreen=/ { print "lastScreen=" v; next }
{ print }
' "$file" > "$file.tmp" && mv "$file.tmp" "$file"
""")
os.chmod(stub, 0o755)
env = {**os.environ, "PATH": stubdir + os.pathsep + os.environ["PATH"]}
def run(*args):
return subprocess.run([sys.executable, SCRIPT, "--file", self.path, "--screens", "3", *args],
capture_output=True, text=True, env=env)
r = run()
self.assertEqual(r.returncode, 0, r.stderr)
with open(self.path + ".ft-bak") as f:
self.assertEqual(f.read(), ISSUE_18)
# The desktop drifts: the panel is saved on the lost screen again, and with a
# different widget. A second repair runs — the backup must still hold the
# config as it was before the first repair, not this drifted state.
drifted = ISSUE_18.replace("org.kde.plasma.kickoff", "org.kde.plasma.trash")
with open(self.path, "w") as f:
f.write(drifted)
r = run()
self.assertEqual(r.returncode, 0, r.stderr)
with open(self.path + ".ft-bak") as f:
self.assertEqual(f.read(), ISSUE_18) # still the pre-first-repair original
with open(self.path) as f:
self.assertEqual(fp.parse(f.read())[("Containments", "10")]["lastScreen"], "0")
if __name__ == "__main__":
unittest.main()