aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--appearance/AppearancePanel.qml31
-rw-r--r--appearance/Icons.qml170
-rw-r--r--appearance/IconsTab.qml177
-rw-r--r--appearance/README.md21
4 files changed, 270 insertions, 129 deletions
diff --git a/appearance/AppearancePanel.qml b/appearance/AppearancePanel.qml
index e034fda..42bc454 100644
--- a/appearance/AppearancePanel.qml
+++ b/appearance/AppearancePanel.qml
@@ -131,19 +131,30 @@ Scope {
anchors.margins: 20
spacing: 16
- Row {
- spacing: 8
- Repeater {
- model: root.tabs
- Tab {
- required property var modelData
- text: root.tabLabels[modelData]
- selected: root.tab === modelData
- onClicked: root.tab = modelData
+ // Centred against the panel, not against the hint: the row
+ // reads as the drawer's own chrome rather than the first
+ // line of a tab's content.
+ Item {
+ width: parent.width
+ height: tabBar.implicitHeight
+
+ Row {
+ id: tabBar
+ anchors.horizontalCenter: parent.horizontalCenter
+ spacing: 8
+ Repeater {
+ model: root.tabs
+ Tab {
+ required property var modelData
+ text: root.tabLabels[modelData]
+ selected: root.tab === modelData
+ onClicked: root.tab = modelData
+ }
}
}
- Item { width: 12; height: 1 }
+
Text {
+ anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: "Tab switches · Esc closes"
font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
diff --git a/appearance/Icons.qml b/appearance/Icons.qml
index 690fff2..2149f6f 100644
--- a/appearance/Icons.qml
+++ b/appearance/Icons.qml
@@ -38,11 +38,74 @@ for name in sys.argv[1:]:
print("%s\\t%s\\t%s" % (name, s, info.get_filename()))
print("%s\\tEND\\t" % name)
`
+ // Four shapes per theme, so a card shows the cases that matter (arrow,
+ // hand, text, horizontal resize) instead of one ambiguous pointer. Each
+ // shape falls back through common aliases and through every storage form a
+ // theme may use: a hyprcursor .hlc zip, a shape directory with an SVG, or
+ // a legacy Xcursor binary that goes through xcur2png.
+ readonly property string cursorScript: `
+import sys, os, glob, shutil, zipfile, subprocess
+outdir = sys.argv[1]
+SAMPLES = {
+ "left_ptr": ["left_ptr", "default", "pointer"],
+ "hand2": ["hand2", "pointer", "hand"],
+ "xterm": ["xterm", "text", "ibeam"],
+ "resize": ["sb_h_double_arrow", "size_hor", "ew-resize", "h_double_arrow"],
+}
+roots = [os.path.expanduser("~/.icons"),
+ os.path.expanduser("~/.local/share/icons"),
+ "/usr/share/icons"]
+
+def pick(d, names):
+ for n in names:
+ for cand in (f"{d}/hyprcursors/{n}.hlc", f"{d}/hyprcursors/{n}/{n}.svg",
+ f"{d}/{n}.hlc", f"{d}/{n}/{n}.svg", f"{d}/cursors/{n}"):
+ if os.path.isfile(cand):
+ return cand
+ return None
+
+os.makedirs(outdir, exist_ok=True)
+for name in sys.argv[2:]:
+ d = next((os.path.join(r, name) for r in roots if os.path.isdir(os.path.join(r, name))), None)
+ if not d:
+ continue
+ for shape, aliases in SAMPLES.items():
+ c = pick(d, aliases)
+ if not c:
+ continue
+ out = f"{outdir}/{name}-{shape}.png"
+ try:
+ if c.endswith(".hlc"):
+ with zipfile.ZipFile(c) as z:
+ members = [e for e in z.namelist() if e.endswith(".svg")]
+ if not members:
+ members = [e for e in z.namelist() if e.endswith(".png")]
+ if not members:
+ continue
+ with open(out, "wb") as f:
+ f.write(z.read(members[0]))
+ elif c.endswith(".svg"):
+ shutil.copyfile(c, out)
+ else:
+ raw = f"{outdir}/raw-{name}-{shape}"
+ shutil.rmtree(raw, ignore_errors=True)
+ os.makedirs(raw, exist_ok=True)
+ subprocess.run(["xcur2png", "-d", raw, "-c", f"{raw}/out.conf", c],
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ pngs = sorted(glob.glob(f"{raw}/*_*.png"))
+ if not pngs:
+ continue
+ shutil.copyfile(pngs[-1], out)
+ print(f"{name}\\t{shape}\\t{out}")
+ except Exception:
+ continue
+`
property var iconThemes: []
property var cursorThemes: []
property string currentIcon: ""
property string currentCursor: ""
+ property int cursorSize: 24
property var iconPreview: ({})
property var cursorPreview: ({})
property bool scanning: true
@@ -92,6 +155,7 @@ for name in sys.argv[1:]:
command: ["sh", "-c",
`for d in ${root.home}/.icons/* ${root.home}/.local/share/icons/* ` +
`/usr/share/icons/*; do [ -d "$d" ] || continue; ` +
+ `[ -L "$d" ] && continue; ` +
`i=0; c=0; m=0; dirs=""; ` +
`[ -d "$d/cursors" ] && c=1; ` +
`[ -f "$d/manifest.hl" ] && m=1; ` +
@@ -111,6 +175,7 @@ for name in sys.argv[1:]:
root.cursorThemes = r.cursors;
root.scanning = false;
if (root.iconThemes.length) root.previewIcons();
+ if (root.cursorThemes.length) root.previewCursors();
}
}
}
@@ -144,54 +209,47 @@ for name in sys.argv[1:]:
id: curProc
command: ["sh", "-c",
`gsettings get org.gnome.desktop.interface icon-theme; ` +
- `gsettings get org.gnome.desktop.interface cursor-theme`]
+ // The system's cursor theme is often the `default` alias, a symlink
+ // to the real theme. The scan skips symlinked dirs, so resolve the
+ // current value to the target name or nothing would read as current.
+ `ct=$(gsettings get org.gnome.desktop.interface cursor-theme | sed "s/'//g"); ` +
+ `for r in ${root.home}/.icons ${root.home}/.local/share/icons /usr/share/icons; do ` +
+ `[ -L "$r/$ct" ] && ct=$(basename "$(readlink -f "$r/$ct")") && break; done; ` +
+ `printf '%s\\n' "$ct"; ` +
+ `gsettings get org.gnome.desktop.interface cursor-size`]
stdout: StdioCollector {
onStreamFinished: {
const lines = text.trim().split("\n");
root.currentIcon = (lines[0] ?? "").replace(/'/g, "");
root.currentCursor = (lines[1] ?? "").replace(/'/g, "");
+ const size = parseInt(lines[2] ?? "", 10);
+ if (!isNaN(size)) root.cursorSize = size;
}
}
}
- // A cursor theme is a manifest + .hlc shapes, a shape directory with SVGs,
- // or a legacy Xcursor cursors/ directory. The extracted image goes to the
- // per-shell cache; on failure the tab shows nothing rather than a broken
- // image.
- function previewCursor(name) {
- const dir = Quickshell.cachePath("cursors");
- const out = `${dir}/${name}.png`;
- curPreviewProc.themeName = name;
- curPreviewProc.command = ["sh", "-c",
- `set -e; ` +
- `d=""; for r in ${root.home}/.icons ${root.home}/.local/share/icons /usr/share/icons; do ` +
- `[ -d "$r/${name}" ] && d="$r/${name}" && break; done; [ -n "$d" ] || exit 1; ` +
- `mkdir -p ${dir}; ` +
- `c=""; for n in left_ptr default pointer hand2; do ` +
- `if [ -f "$d/hyprcursors/$n.hlc" ]; then c="$d/hyprcursors/$n.hlc"; break; fi; ` +
- `if [ -f "$d/hyprcursors/$n/$n.svg" ]; then c="$d/hyprcursors/$n/$n.svg"; break; fi; ` +
- `if [ -f "$d/$n.hlc" ]; then c="$d/$n.hlc"; break; fi; ` +
- `if [ -f "$d/$n/$n.svg" ]; then c="$d/$n/$n.svg"; break; fi; ` +
- `if [ -f "$d/cursors/$n" ]; then c="$d/cursors/$n"; break; fi; done; ` +
- `[ -n "$c" ] || exit 1; ` +
- `case "$c" in ` +
- `*.hlc) unzip -p "$c" '*.svg' > ${out} 2>/dev/null; ` +
- `[ -s ${out} ] || unzip -p "$c" '*.png' > ${out} 2>/dev/null; ` +
- `[ -s ${out} ] || exit 1;; ` +
- `*.svg) cp "$c" ${out};; ` +
- `*) rm -rf ${dir}/raw-${name}; mkdir -p ${dir}/raw-${name}; ` +
- `xcur2png -d ${dir}/raw-${name} -c ${dir}/raw-${name}/out.conf "$c" >/dev/null 2>&1; ` +
- `cp "$(ls ${dir}/raw-${name}/$(basename "$c")_*.png | tail -1)" ${out};; esac`]
- curPreviewProc.running = false;
- curPreviewProc.running = true;
+ // One python pass extracts four shapes for EVERY cursor theme and prints
+ // name<TAB>shape<TAB>path, so the whole row paints on open instead of
+ // waiting for a hover per card. A theme missing a shape simply shows fewer
+ // images rather than a broken one.
+ function previewCursors() {
+ if (!root.cursorThemes.length) return;
+ cursorProc.command = ["python3", "-c", root.cursorScript,
+ Quickshell.cachePath("cursors")].concat(root.cursorThemes);
+ cursorProc.running = false;
+ cursorProc.running = true;
}
Process {
- id: curPreviewProc
- property string themeName: ""
- onExited: code => {
- if (code === 0) {
- const next = Object.assign({}, root.cursorPreview);
- next[themeName] = `${Quickshell.cachePath("cursors")}/${themeName}.png`;
+ id: cursorProc
+ stdout: StdioCollector {
+ onStreamFinished: {
+ const next = {};
+ for (const line of text.split("\n")) {
+ const p = line.split("\t");
+ if (p.length < 3 || !p[0] || !p[1] || !p[2]) continue;
+ if (!next[p[0]]) next[p[0]] = {};
+ next[p[0]][p[1]] = p[2];
+ }
root.cursorPreview = next;
}
}
@@ -202,6 +260,21 @@ for name in sys.argv[1:]:
FileView { id: qt6File; path: `${root.home}/.config/qt6ct/qt6ct.conf`; blockLoading: true }
FileView { id: qt5File; path: `${root.home}/.config/qt5ct/qt5ct.conf`; blockLoading: true }
FileView { id: envFile; path: `${root.home}/.config/hypr/sections/environment.lua`; blockLoading: true }
+ // GTK3 and GTK4 read their own settings.ini, with the theme names written
+ // out; gsettings alone does not switch them.
+ FileView { id: gtk3File; path: `${root.home}/.config/gtk-3.0/settings.ini`; blockLoading: true }
+ FileView { id: gtk4File; path: `${root.home}/.config/gtk-4.0/settings.ini`; blockLoading: true }
+
+ // Replaces a `key=value` line in both GTK settings files. A key the file
+ // does not carry is left alone, the same as the Qt configs.
+ function rewriteGtk(key, value) {
+ const files = [gtk3File, gtk4File];
+ const re = new RegExp(`^${key}=.*`, "m");
+ for (let i = 0; i < files.length; i++) {
+ const text = files[i].text();
+ if (text !== "") files[i].setText(text.replace(re, `${key}=${value}`));
+ }
+ }
function gsettingsSet(key, value) {
gsetProc.command = ["gsettings", "set", "org.gnome.desktop.interface", key, value];
@@ -217,20 +290,37 @@ for name in sys.argv[1:]:
const text = files[i].text();
if (text !== "") files[i].setText(text.replace(/^icon_theme=.*/m, `icon_theme=${name}`));
}
+ root.rewriteGtk("gtk-icon-theme-name", name);
root.currentIcon = name;
root.notice = `${name} set. Restart apps to see it.`;
}
function applyCursor(name) {
// Live switch first, then persistence.
- Quickshell.execDetached(["hyprctl", "setcursor", name, "24"]);
+ Quickshell.execDetached(["hyprctl", "setcursor", name, String(root.cursorSize)]);
root.gsettingsSet("cursor-theme", name);
const text = envFile.text();
// Matches both XCURSOR_THEME and HYPRCURSOR_THEME: both end in
// CURSOR_THEME", ".
if (text !== "")
- envFile.setText(text.replace(/(CURSOR_THEME", ")[^"]*/g, (m, p1) => p1 + name));
+ envFile.setText(text.replace(/(CURSOR_THEME", ")[^"]*/g, (m, p1) => p1 + name)
+ .replace(/(CURSOR_SIZE", ")[^"]*/g, (m, p1) => p1 + root.cursorSize));
+ root.rewriteGtk("gtk-cursor-theme-name", name);
+ root.rewriteGtk("gtk-cursor-theme-size", root.cursorSize);
root.currentCursor = name;
- root.notice = `${name} set live. environment.lua updated for next login.`;
+ root.notice = `${name} set live at ${root.cursorSize}px. environment.lua updated for next login.`;
+ }
+
+ function setCursorSize(size) {
+ root.cursorSize = size;
+ // Live on the current theme, then GTK and the next login's env.
+ Quickshell.execDetached(["hyprctl", "setcursor", root.currentCursor, String(size)]);
+ root.gsettingsSet("cursor-size", String(size));
+ const text = envFile.text();
+ // Matches both XCURSOR_SIZE and HYPRCURSOR_SIZE.
+ if (text !== "")
+ envFile.setText(text.replace(/(CURSOR_SIZE", ")[^"]*/g, (m, p1) => p1 + size));
+ root.rewriteGtk("gtk-cursor-theme-size", size);
+ root.notice = `cursor size ${size}px. environment.lua updated for next login.`;
}
}
diff --git a/appearance/IconsTab.qml b/appearance/IconsTab.qml
index 92c3f74..b2c7ee4 100644
--- a/appearance/IconsTab.qml
+++ b/appearance/IconsTab.qml
@@ -13,32 +13,33 @@ import Quickshell
import Quickshell.Widgets
import QtQuick
-Column {
- spacing: 12
+Flickable {
+ contentHeight: col.implicitHeight
+ clip: true
- Text {
- text: Icons.scanning ? "scanning…" : `${Icons.iconThemes.length} icon themes · ${Icons.cursorThemes.length} cursor themes`
- color: Theme.overlay
- font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
- }
-
- Text {
- text: "Icon theme"
- color: Theme.subtext
- font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true }
- }
+ property string liveCursorCursor: ""
- Flickable {
- id: iconList
+ Column {
+ id: col
width: parent.width
- height: 150
- contentWidth: iconRow.implicitWidth
- contentHeight: height
- clip: true
- flickableDirection: Flickable.HorizontalFlick
+ spacing: 12
- Row {
- id: iconRow
+ Text {
+ text: Icons.scanning ? "scanning…" : `${Icons.iconThemes.length} icon themes · ${Icons.cursorThemes.length} cursor themes`
+ color: Theme.overlay
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
+ }
+
+ Text {
+ text: "Icon theme"
+ color: Theme.subtext
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true }
+ }
+
+ // Flow, not a horizontal strip: the cards wrap onto further rows rather
+ // than running off the right edge, and the tab scrolls vertically.
+ Flow {
+ width: parent.width
spacing: 10
Repeater {
model: Icons.iconThemes
@@ -46,7 +47,7 @@ Column {
id: iconCard
required property string modelData
readonly property bool current: Icons.currentIcon === modelData
- width: 150; height: 120; radius: 10
+ width: 230; height: 150; radius: 10
color: current ? Qt.alpha(Theme.accent, 0.2) : Qt.alpha(Theme.surface, 0.35)
border.width: current ? 2 : 1
border.color: current ? Theme.accent : "transparent"
@@ -58,10 +59,12 @@ Column {
}
Column {
+ width: parent.width - 16
anchors.centerIn: parent
- spacing: 6
+ spacing: 10
Row {
- spacing: 6
+ anchors.horizontalCenter: parent.horizontalCenter
+ spacing: 8
Repeater {
model: ["folder", "text-x-generic", "image-x-generic", "network-wireless"]
IconImage {
@@ -73,45 +76,62 @@ Column {
// /org/gtk gresource paths Qt cannot open.
source: glyph.indexOf("/") === 0 && glyph.indexOf("/org/gtk/") !== 0
? "file://" + glyph : ""
- implicitSize: 26
+ implicitSize: 44
}
}
}
Text {
- anchors.horizontalCenter: parent.horizontalCenter
+ width: parent.width
+ horizontalAlignment: Text.AlignHCenter
text: iconCard.current ? modelData + " current" : modelData
color: Theme.text
- font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 }
+ wrapMode: Text.WrapAnywhere
+ maximumLineCount: 2
+ elide: Text.ElideRight
}
}
}
}
}
- }
-
- Text {
- text: "Cursor theme"
- color: Theme.subtext
- font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true }
- }
-
- Flickable {
- width: parent.width
- height: 150
- contentWidth: cursorRow.implicitWidth
- contentHeight: height
- clip: true
- flickableDirection: Flickable.HorizontalFlick
Row {
- id: cursorRow
+ spacing: 10
+ Text {
+ anchors.verticalCenter: parent.verticalCenter
+ text: "Cursor theme"
+ color: Theme.subtext
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true }
+ }
+ Item { width: 12; height: 1 }
+ Text {
+ anchors.verticalCenter: parent.verticalCenter
+ text: "Size"
+ color: Theme.subtext
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 }
+ }
+ Repeater {
+ model: [16, 24, 32, 48, 64]
+ Tab {
+ required property var modelData
+ anchors.verticalCenter: parent.verticalCenter
+ text: String(modelData)
+ selected: Icons.cursorSize === modelData
+ onClicked: Icons.setCursorSize(modelData)
+ }
+ }
+ }
+
+ Flow {
+ width: parent.width
spacing: 10
Repeater {
model: Icons.cursorThemes
Rectangle {
+ id: cursorCard
required property string modelData
readonly property bool current: Icons.currentCursor === modelData
- width: 120; height: 120; radius: 10
+ width: 230; height: 150; radius: 10
color: current ? Qt.alpha(Theme.accent, 0.2) : Qt.alpha(Theme.surface, 0.35)
border.width: current ? 2 : 1
border.color: current ? Theme.accent : "transparent"
@@ -119,13 +139,15 @@ Column {
HoverHandler {
onHoveredChanged: {
if (hovered) {
- Icons.previewCursor(modelData);
- // Live preview: the real cursor, reverted on leave.
+ // Previews are already batched on open; hovering
+ // only switches the real cursor and reverts it.
liveCursorCursor = modelData;
- Quickshell.execDetached(["hyprctl", "setcursor", modelData, "24"]);
+ Quickshell.execDetached(["hyprctl", "setcursor", modelData,
+ String(Icons.cursorSize)]);
} else if (liveCursorCursor === modelData) {
liveCursorCursor = "";
- Quickshell.execDetached(["hyprctl", "setcursor", Icons.currentCursor, "24"]);
+ Quickshell.execDetached(["hyprctl", "setcursor", Icons.currentCursor,
+ String(Icons.cursorSize)]);
}
}
}
@@ -136,49 +158,60 @@ Column {
}
Column {
+ width: parent.width - 16
anchors.centerIn: parent
- spacing: 6
- Image {
+ spacing: 10
+ Row {
anchors.horizontalCenter: parent.horizontalCenter
- width: 40; height: 40
- asynchronous: true
- source: Icons.cursorPreview[modelData] ? "file://" + Icons.cursorPreview[modelData] : ""
- fillMode: Image.PreserveAspectFit
+ spacing: 8
+ Repeater {
+ // The cases that tell themes apart: arrow, hand,
+ // text and horizontal resize.
+ model: ["left_ptr", "hand2", "xterm", "resize"]
+ Image {
+ required property string modelData
+ readonly property string p: (Icons.cursorPreview[cursorCard.modelData] ?? {})[modelData] ?? ""
+ width: 44; height: 44
+ asynchronous: true
+ source: p !== "" ? "file://" + p : ""
+ fillMode: Image.PreserveAspectFit
+ }
+ }
}
Text {
- anchors.horizontalCenter: parent.horizontalCenter
- text: parent.parent.current ? modelData + " current" : modelData
- color: Theme.text
- font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
- width: 110
- elide: Text.ElideMiddle
+ width: parent.width
horizontalAlignment: Text.AlignHCenter
+ text: cursorCard.current ? modelData + " current" : modelData
+ color: Theme.text
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 }
+ wrapMode: Text.WrapAnywhere
+ maximumLineCount: 2
+ elide: Text.ElideRight
}
}
}
}
}
- }
- Text {
- width: parent.width
- wrapMode: Text.Wrap
- visible: Icons.notice !== ""
- text: Icons.notice
- color: Theme.green
- font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 }
+ Text {
+ width: parent.width
+ wrapMode: Text.Wrap
+ visible: Icons.notice !== ""
+ text: Icons.notice
+ color: Theme.green
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 }
+ }
}
- property string liveCursorCursor: ""
onVisibleChanged: if (!visible && liveCursorCursor !== "") {
- Quickshell.execDetached(["hyprctl", "setcursor", Icons.currentCursor, "24"]);
+ Quickshell.execDetached(["hyprctl", "setcursor", Icons.currentCursor, String(Icons.cursorSize)]);
liveCursorCursor = "";
}
// A Loader destroys this item on tab switch or drawer close, and visible
// does not necessarily change first, so revert the real cursor here too.
Component.onDestruction: if (liveCursorCursor !== "") {
- Quickshell.execDetached(["hyprctl", "setcursor", Icons.currentCursor, "24"]);
+ Quickshell.execDetached(["hyprctl", "setcursor", Icons.currentCursor, String(Icons.cursorSize)]);
liveCursorCursor = "";
}
}
diff --git a/appearance/README.md b/appearance/README.md
index 9e1a41c..85fe5d7 100644
--- a/appearance/README.md
+++ b/appearance/README.md
@@ -44,13 +44,20 @@ written commented out. Save restarts `hypridle`, which resets its timers.
## Icons
-Switches the icon and cursor theme. Icon previews come from a GTK lookup per
-theme; cursor previews extract `left_ptr` from a hyprcursor `.hlc` with
-`unzip` or from an Xcursor theme with `xcur2png`, and hovering changes the real
-cursor. Applying writes `gsettings`, the Qt configs, and for cursors
-`environment.lua`; apps and a relogin are needed to see the rest. The theme
-name is still hardcoded in `unified-desktop-theme`, `waybar-theme-udt` and
-rofi, which is tracked as a follow-up in those repos.
+Switches the icon and cursor theme, and the pointer size. Icon previews come
+from a GTK lookup per theme. Cursor previews come from one python pass per
+open: for every theme it extracts four shapes (arrow, hand, text, horizontal
+resize) out of a hyprcursor `.hlc`, a shape directory's SVG, or a legacy
+Xcursor binary via `xcur2png`, so a card shows the cases that tell themes
+apart. Hovering a card changes the real cursor and reverts it on leave.
+
+Applying writes `gsettings` (which GTK3 and GTK4 both read), the Qt configs
+(`qt6ct`/`qt5ct`), and, for cursors, `environment.lua`. The GTK theme names do
+not come from `gsettings` alone here, so the drawer also rewrites
+`gtk-3.0/settings.ini` and `gtk-4.0/settings.ini` to match. Apps and a relogin
+are needed to see the rest. The theme name is still hardcoded in
+`unified-desktop-theme`, `waybar-theme-udt` and rofi, which is tracked as a
+follow-up in those repos.
## Wallpapers