fix: a desktop Plasma makes after the layout has run gets the classroom's wallpaper
Tiny Pebbles came through the 0.2.2 upgrade on Plasma's own purple wallpaper: its desktop was containment 10, made after the layout had run (most likely the television arriving after a night-time reboot), with no wallpaper configured. The look and feel's fallback could not catch it, because it named a file and Plasma only accepts a wallpaper package there. The fallback is now the EkklesiaClassroom package, whose one image links to the classroom's picture. classroom-desktop-wallpaper, run before every plasmashell start, writes the picture into every desktop containment the file already has: found by plugin, never by number; panels and everything else byte for byte; a file that is already right not written at all. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
530f6f6563
commit
7b4ac39cd2
@@ -24,8 +24,18 @@ cursorTheme=breeze_cursors
|
|||||||
library=org.kde.breeze
|
library=org.kde.breeze
|
||||||
theme=Breeze
|
theme=Breeze
|
||||||
|
|
||||||
|
# The picture a desktop gets when nothing has said which. That is every
|
||||||
|
# desktop Plasma makes on its own after the layout has run: a television
|
||||||
|
# switched on after the computer, a second screen, a new activity.
|
||||||
|
#
|
||||||
|
# A wallpaper PACKAGE NAME, not a file. Plasma's DefaultWallpaper looks it
|
||||||
|
# up as wallpapers/<name> and nothing else. This line was a path until
|
||||||
|
# 0.2.6.6, which Plasma cannot load as a package, so every such desktop
|
||||||
|
# fell back to Plasma's own purple one — measured, and it is what Tiny
|
||||||
|
# Pebbles showed after the 0.2.2 upgrade. The package is a directory whose
|
||||||
|
# one image is a link to the classroom's file; see build-release.sh.
|
||||||
[Wallpaper]
|
[Wallpaper]
|
||||||
Image=/usr/share/backgrounds/ekklesia/classroom-calm.png
|
Image=EkklesiaClassroom
|
||||||
|
|
||||||
[KSplash]
|
[KSplash]
|
||||||
Theme=None
|
Theme=None
|
||||||
|
|||||||
@@ -37,6 +37,13 @@ var WALLPAPER = "/usr/share/backgrounds/ekklesia/classroom-calm.png";
|
|||||||
var PANEL_HEIGHT = 76;
|
var PANEL_HEIGHT = 76;
|
||||||
|
|
||||||
// The idle desktop: the wallpaper, and nothing on it.
|
// The idle desktop: the wallpaper, and nothing on it.
|
||||||
|
//
|
||||||
|
// Only the desktops that exist right now. A screen that appears later —
|
||||||
|
// the television switched on after a reboot at three in the morning, which
|
||||||
|
// is what Tiny Pebbles had — gets a desktop Plasma makes without this
|
||||||
|
// file. That one takes the look and feel's default wallpaper, which is the
|
||||||
|
// classroom's package, and classroom-desktop-wallpaper writes it in by
|
||||||
|
// name before the shell next starts.
|
||||||
var screens = desktops();
|
var screens = desktops();
|
||||||
for (var i = 0; i < screens.length; i++) {
|
for (var i = 0; i < screens.length; i++) {
|
||||||
var desktop = screens[i];
|
var desktop = screens[i];
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"KPlugin": {
|
||||||
|
"Authors": [
|
||||||
|
{
|
||||||
|
"Name": "Ekklesia"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Id": "EkklesiaClassroom",
|
||||||
|
"License": "GPLv2+",
|
||||||
|
"Name": "Ekklesia Classroom"
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+308
@@ -0,0 +1,308 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
"""Put the classroom's wallpaper on every desktop the shell has.
|
||||||
|
|
||||||
|
Tiny Pebbles came up after the 0.2.2 -> 0.2.6.4 upgrade on Plasma's own
|
||||||
|
purple wallpaper, with the right panel and the right package installed.
|
||||||
|
Its desktop was containment 10, with no Wallpaper group at all:
|
||||||
|
|
||||||
|
[Containments][10]
|
||||||
|
plugin=org.kde.plasma.folder
|
||||||
|
wallpaperplugin=org.kde.image
|
||||||
|
[Containments][10][General]
|
||||||
|
positions={"1920x1080":[]}
|
||||||
|
|
||||||
|
The layout script writes the wallpaper, but only onto the desktops that
|
||||||
|
exist while it runs. Plasma makes a desktop for every screen, and every
|
||||||
|
activity, that appears afterwards — and it makes them without the layout.
|
||||||
|
0.2.2's updater rebooted at three in the morning with the television
|
||||||
|
switched off at the set, so the layout ran with no screen and no desktop
|
||||||
|
at all, and the desktop Plasma made when the television came on was
|
||||||
|
bare. Reproduced in the lab through a new activity, which takes the same
|
||||||
|
path through the shell: containment 11, the same four lines, Plasma's
|
||||||
|
wallpaper.
|
||||||
|
|
||||||
|
Two things now close that. The look and feel's default wallpaper is a
|
||||||
|
real wallpaper package, so a desktop Plasma makes on its own comes up
|
||||||
|
with the classroom's picture — that is the fix, and it needs nothing to
|
||||||
|
run. And this, which writes the picture into every desktop the file
|
||||||
|
already has, so that the configuration says what the screen shows.
|
||||||
|
|
||||||
|
WHICH CONTAINMENT IS THE DESKTOP is answered by what it is, never by its
|
||||||
|
number: a top-level containment whose plugin is one of Plasma's desktop
|
||||||
|
containments. Tiny Pebbles' desktop was 10 and River Kids' is 1, and the
|
||||||
|
panel's number is whatever was free when it was built.
|
||||||
|
|
||||||
|
ONLY THE DESKTOPS ARE TOUCHED. Every panel, applet and line that is not a
|
||||||
|
desktop's wallpaper is written back byte for byte, and a file that is
|
||||||
|
already right is not written at all.
|
||||||
|
|
||||||
|
WHEN IT RUNS. Before plasmashell starts, as the shell's own ExecStartPre,
|
||||||
|
because the shell owns this file: editing it underneath a live shell was
|
||||||
|
measured to destroy the panel. That is every start — a login, a reboot,
|
||||||
|
and the restart after a crash — and at a login it usually finds nothing,
|
||||||
|
because classroom-desktop-reset has just removed the file for the layout
|
||||||
|
to rebuild. Run by hand, it refuses while the shell is running and says
|
||||||
|
how to apply it. Live D-Bus scripting is not an option: the classroom
|
||||||
|
locks the desktop and forbids the scripting console, and Plasma answers
|
||||||
|
"Widgets are locked" — measured.
|
||||||
|
|
||||||
|
classroom-desktop-wallpaper repair, if the shell is stopped
|
||||||
|
classroom-desktop-wallpaper --check say what is wrong, change nothing
|
||||||
|
classroom-desktop-wallpaper FILE work on FILE instead
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
WALLPAPER = "/usr/share/backgrounds/ekklesia/classroom-calm.png"
|
||||||
|
|
||||||
|
WALLPAPER_PLUGIN = "org.kde.image"
|
||||||
|
|
||||||
|
# Plasma's two desktop containments: Folder View, which is what the
|
||||||
|
# classroom's desktops are, and the plain Desktop. Panels, the system tray
|
||||||
|
# a 0.2.2-era profile may still carry and anything else are not desktops.
|
||||||
|
DESKTOP_PLUGINS = ("org.kde.plasma.folder", "org.kde.desktopcontainment")
|
||||||
|
|
||||||
|
# What the layout writes onto a desktop, so a repaired desktop and a
|
||||||
|
# freshly built one say the same thing.
|
||||||
|
WALLPAPER_KEYS = (
|
||||||
|
("FillMode", "2"),
|
||||||
|
("Image", f"file://{WALLPAPER}"),
|
||||||
|
)
|
||||||
|
|
||||||
|
APPLETSRC = "plasma-org.kde.plasma.desktop-appletsrc"
|
||||||
|
|
||||||
|
CONTAINMENT = re.compile(r"^\[Containments\]\[(\d+)\]$")
|
||||||
|
|
||||||
|
# key=value, with KConfig's optional [$i]-style flags or [locale] after
|
||||||
|
# the key. The flags are kept when a value is replaced.
|
||||||
|
ENTRY = re.compile(r"^([^=\[\s]+)((?:\[[^\]]*\])*)=(.*)$")
|
||||||
|
|
||||||
|
|
||||||
|
def say(message):
|
||||||
|
print(f"classroom-desktop-wallpaper: {message}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def groups(lines):
|
||||||
|
"""[(name, header, end)]: lines[header] is the group's header line, or
|
||||||
|
-1 for the lines before the first group, and end is exclusive."""
|
||||||
|
found = []
|
||||||
|
name, header = None, -1
|
||||||
|
|
||||||
|
for index, line in enumerate(lines):
|
||||||
|
stripped = line.strip()
|
||||||
|
|
||||||
|
if stripped.startswith("[") and stripped.endswith("]"):
|
||||||
|
found.append((name, header, index))
|
||||||
|
name, header = stripped, index
|
||||||
|
|
||||||
|
found.append((name, header, len(lines)))
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def find_group(lines, name):
|
||||||
|
for group, header, end in groups(lines):
|
||||||
|
if group == name:
|
||||||
|
return header, end
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def entry(line):
|
||||||
|
match = ENTRY.match(line.rstrip("\n"))
|
||||||
|
return (match.group(1), match.group(2), match.group(3)) if match else None
|
||||||
|
|
||||||
|
|
||||||
|
def desktops(lines):
|
||||||
|
"""The ids of the desktop containments, whatever they are numbered."""
|
||||||
|
ids = []
|
||||||
|
|
||||||
|
for name, header, end in groups(lines):
|
||||||
|
match = CONTAINMENT.match(name or "")
|
||||||
|
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for line in lines[header + 1:end]:
|
||||||
|
parsed = entry(line)
|
||||||
|
|
||||||
|
if parsed and parsed[0] == "plugin" and parsed[2] in DESKTOP_PLUGINS:
|
||||||
|
ids.append(match.group(1))
|
||||||
|
break
|
||||||
|
|
||||||
|
return ids
|
||||||
|
|
||||||
|
|
||||||
|
def set_entry(lines, group, key, value):
|
||||||
|
"""Make `key=value` true in an existing group. Returns what changed,
|
||||||
|
or None when it already was."""
|
||||||
|
header, end = find_group(lines, group)
|
||||||
|
last = header
|
||||||
|
|
||||||
|
for index in range(header + 1, end):
|
||||||
|
parsed = entry(lines[index])
|
||||||
|
|
||||||
|
if lines[index].strip():
|
||||||
|
last = index
|
||||||
|
|
||||||
|
if parsed and parsed[0] == key:
|
||||||
|
if parsed[2] == value:
|
||||||
|
return None
|
||||||
|
|
||||||
|
lines[index] = f"{key}{parsed[1]}={value}\n"
|
||||||
|
return f"{key} was [{parsed[2]}]"
|
||||||
|
|
||||||
|
lines.insert(last + 1, f"{key}={value}\n")
|
||||||
|
return f"{key} was missing"
|
||||||
|
|
||||||
|
|
||||||
|
def add_group(lines, containment, group):
|
||||||
|
"""A new group, placed after the last of its containment's own."""
|
||||||
|
prefix = f"[Containments][{containment}]"
|
||||||
|
after = None
|
||||||
|
|
||||||
|
for name, header, end in groups(lines):
|
||||||
|
if name == prefix or (name or "").startswith(prefix + "["):
|
||||||
|
after = end
|
||||||
|
|
||||||
|
# Back over the blank lines that separate it from the next group, so
|
||||||
|
# the new one sits with its containment and the spacing stays as
|
||||||
|
# KConfig writes it.
|
||||||
|
while after > 0 and not lines[after - 1].strip():
|
||||||
|
after -= 1
|
||||||
|
|
||||||
|
if after > 0 and not lines[after - 1].endswith("\n"):
|
||||||
|
lines[after - 1] += "\n"
|
||||||
|
|
||||||
|
block = ["\n", f"{group}\n"]
|
||||||
|
block += [f"{key}={value}\n" for key, value in WALLPAPER_KEYS]
|
||||||
|
lines[after:after] = block
|
||||||
|
|
||||||
|
|
||||||
|
def converge(text):
|
||||||
|
"""The file as it should be, and what was changed to make it so."""
|
||||||
|
lines = text.splitlines(keepends=True)
|
||||||
|
changes = []
|
||||||
|
|
||||||
|
for containment in desktops(lines):
|
||||||
|
top = f"[Containments][{containment}]"
|
||||||
|
change = set_entry(lines, top, "wallpaperplugin", WALLPAPER_PLUGIN)
|
||||||
|
|
||||||
|
if change:
|
||||||
|
changes.append(f"desktop {containment}: {change}")
|
||||||
|
|
||||||
|
group = f"{top}[Wallpaper][{WALLPAPER_PLUGIN}][General]"
|
||||||
|
|
||||||
|
if find_group(lines, group) is None:
|
||||||
|
add_group(lines, containment, group)
|
||||||
|
changes.append(f"desktop {containment}: had no wallpaper configured")
|
||||||
|
continue
|
||||||
|
|
||||||
|
for key, value in WALLPAPER_KEYS:
|
||||||
|
change = set_entry(lines, group, key, value)
|
||||||
|
|
||||||
|
if change:
|
||||||
|
changes.append(f"desktop {containment}: {change}")
|
||||||
|
|
||||||
|
return "".join(lines), changes
|
||||||
|
|
||||||
|
|
||||||
|
def default_path():
|
||||||
|
home = os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config")
|
||||||
|
return os.path.join(home, APPLETSRC)
|
||||||
|
|
||||||
|
|
||||||
|
def shell_running(proc="/proc"):
|
||||||
|
"""plasmashell running as this user. Read from /proc rather than asked
|
||||||
|
of systemd, so a shell started any way at all counts, and rather than
|
||||||
|
of pgrep, which nothing guarantees is installed."""
|
||||||
|
uid = os.getuid()
|
||||||
|
|
||||||
|
for pid in os.listdir(proc):
|
||||||
|
if not pid.isdigit():
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(os.path.join(proc, pid, "comm"), encoding="utf-8") as handle:
|
||||||
|
name = handle.read().strip()
|
||||||
|
|
||||||
|
if name == "plasmashell" and os.stat(os.path.join(proc, pid)).st_uid == uid:
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
# Gone while it was being looked at.
|
||||||
|
continue
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def write(path, text):
|
||||||
|
"""Replace the file whole, so a full disk or a power cut leaves the old
|
||||||
|
one rather than half of a new one."""
|
||||||
|
temporary = f"{path}.classroom-new"
|
||||||
|
mode = os.stat(path).st_mode & 0o7777
|
||||||
|
|
||||||
|
with open(temporary, "w", encoding="utf-8") as handle:
|
||||||
|
handle.write(text)
|
||||||
|
handle.flush()
|
||||||
|
os.fsync(handle.fileno())
|
||||||
|
|
||||||
|
os.chmod(temporary, mode)
|
||||||
|
os.replace(temporary, path)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
check = "--check" in argv
|
||||||
|
paths = [arg for arg in argv if not arg.startswith("--")]
|
||||||
|
path = paths[0] if paths else default_path()
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8") as handle:
|
||||||
|
before = handle.read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
say("no desktop configuration yet; the layout writes the wallpaper")
|
||||||
|
return 0
|
||||||
|
except OSError as error:
|
||||||
|
say(f"cannot read {path}: {error}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
after, changes = converge(before)
|
||||||
|
found = desktops(before.splitlines(keepends=True))
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
say("no desktop in the configuration yet; Plasma's default is the "
|
||||||
|
"classroom's wallpaper package")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if not changes:
|
||||||
|
say(f"desktop {', '.join(found)}: the classroom wallpaper already")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
for change in changes:
|
||||||
|
say(change)
|
||||||
|
|
||||||
|
if check:
|
||||||
|
say("not changed (--check)")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# The shell owns the live file. Anything else is just a file.
|
||||||
|
if not paths and shell_running():
|
||||||
|
say("plasmashell is running, and editing its file underneath it")
|
||||||
|
say("destroys the panel. Apply it with:")
|
||||||
|
say(" systemctl --user restart plasma-plasmashell.service")
|
||||||
|
say("which repairs the file before the shell starts again.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
write(path, after)
|
||||||
|
except OSError as error:
|
||||||
|
say(f"cannot write {path}: {error}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
say(f"the wallpaper is now configured on desktop {', '.join(found)}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv[1:]))
|
||||||
@@ -16,3 +16,9 @@ StartLimitIntervalSec=0
|
|||||||
[Service]
|
[Service]
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=2
|
RestartSec=2
|
||||||
|
# Every desktop the configuration already has gets the classroom's
|
||||||
|
# wallpaper written into it, in the one moment the shell is not running to
|
||||||
|
# own the file — at a login, and at every restart after a crash. See
|
||||||
|
# classroom-desktop-wallpaper. The "-" is because a panel matters more than
|
||||||
|
# a wallpaper: nothing that script can do is allowed to stop the shell.
|
||||||
|
ExecStartPre=-/usr/local/bin/classroom-desktop-wallpaper
|
||||||
|
|||||||
@@ -176,6 +176,32 @@ install -Dm644 \
|
|||||||
"$ROOT/config/wallpapers/classroom-calm.png" \
|
"$ROOT/config/wallpapers/classroom-calm.png" \
|
||||||
"$STAGE/usr/share/backgrounds/ekklesia/classroom-calm.png"
|
"$STAGE/usr/share/backgrounds/ekklesia/classroom-calm.png"
|
||||||
|
|
||||||
|
# THE SAME PICTURE, AS A WALLPAPER PACKAGE.
|
||||||
|
#
|
||||||
|
# The look and feel names this as the default for any desktop Plasma makes
|
||||||
|
# without the layout, and Plasma only accepts a package there, never a
|
||||||
|
# file. Its one image is a link to the file above rather than a copy, so
|
||||||
|
# there is still exactly one picture, and every desktop the layout wrote
|
||||||
|
# keeps pointing at the path it always has. The name says the image's
|
||||||
|
# size, because that is how Plasma chooses between a package's images.
|
||||||
|
WALLPAPER_PACKAGE="$STAGE/usr/share/wallpapers/EkklesiaClassroom"
|
||||||
|
|
||||||
|
install -Dm644 \
|
||||||
|
"$ROOT/config/wallpapers/EkklesiaClassroom/metadata.json" \
|
||||||
|
"$WALLPAPER_PACKAGE/metadata.json"
|
||||||
|
|
||||||
|
wallpaper_size="$(python3 -c '
|
||||||
|
import struct, sys
|
||||||
|
with open(sys.argv[1], "rb") as png:
|
||||||
|
header = png.read(24)
|
||||||
|
assert header[:8] == b"\x89PNG\r\n\x1a\n" and header[12:16] == b"IHDR"
|
||||||
|
print("%dx%d" % struct.unpack(">II", header[16:24]))
|
||||||
|
' "$ROOT/config/wallpapers/classroom-calm.png")"
|
||||||
|
|
||||||
|
install -d -m755 "$WALLPAPER_PACKAGE/contents" "$WALLPAPER_PACKAGE/contents/images"
|
||||||
|
ln -s /usr/share/backgrounds/ekklesia/classroom-calm.png \
|
||||||
|
"$WALLPAPER_PACKAGE/contents/images/${wallpaper_size}.png"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -466,6 +492,10 @@ install -Dm755 \
|
|||||||
"$ROOT/scripts/classroom-desktop-reset" \
|
"$ROOT/scripts/classroom-desktop-reset" \
|
||||||
"$STAGE/usr/local/bin/classroom-desktop-reset"
|
"$STAGE/usr/local/bin/classroom-desktop-reset"
|
||||||
|
|
||||||
|
install -Dm755 \
|
||||||
|
"$ROOT/scripts/classroom-desktop-wallpaper" \
|
||||||
|
"$STAGE/usr/local/bin/classroom-desktop-wallpaper"
|
||||||
|
|
||||||
install -Dm755 \
|
install -Dm755 \
|
||||||
"$ROOT/scripts/classroom-theme" \
|
"$ROOT/scripts/classroom-theme" \
|
||||||
"$STAGE/usr/local/bin/classroom-theme"
|
"$STAGE/usr/local/bin/classroom-theme"
|
||||||
|
|||||||
Reference in New Issue
Block a user