mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-18 17:24:01 -04:00
icons: honor XDG base dir order & resolve standalone icons
- User icon dirs now sort ahead of system ones so overrides in ~/.icons and ~/.local/share/icons - Desktop entry changes refresh that index and drop cached misses instead of clearing everything Closes #2360 Closes #3246
This commit is contained in:
@@ -23,19 +23,45 @@ Singleton {
|
||||
property var _searchDirs: []
|
||||
property string _dirsForTheme: ""
|
||||
property var _cache: ({})
|
||||
property var _looseIconPaths: ({})
|
||||
property int _cacheGeneration: 0
|
||||
property int _rebuildGeneration: 0
|
||||
property int _looseIndexGeneration: 0
|
||||
property int revision: 0
|
||||
property bool _bumpPending: false
|
||||
|
||||
readonly property var _baseDirs: {
|
||||
const xdg = Quickshell.env("XDG_DATA_DIRS") || "";
|
||||
// XDG icon spec order: user dirs win over system ones, so overrides in ~/.local/share/icons beat installed themes.
|
||||
readonly property var _userIconRoots: {
|
||||
const localData = Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation));
|
||||
const home = Paths.strip(StandardPaths.writableLocation(StandardPaths.HomeLocation));
|
||||
const dataDirs = xdg.trim() !== "" ? xdg.split(":").concat([localData]) : ["/usr/share", "/usr/local/share", localData];
|
||||
return [home + "/.icons", localData + "/icons"];
|
||||
}
|
||||
|
||||
readonly property var _dataDirs: {
|
||||
const xdg = Quickshell.env("XDG_DATA_DIRS") || "";
|
||||
const localData = Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation));
|
||||
const dataDirs = xdg.trim() !== "" ? [localData].concat(xdg.split(":")) : [localData, "/usr/local/share", "/usr/share"];
|
||||
for (const flatpak of [localData + "/flatpak/exports/share", "/var/lib/flatpak/exports/share"]) {
|
||||
if (!dataDirs.includes(flatpak))
|
||||
dataDirs.push(flatpak);
|
||||
}
|
||||
return dataDirs.map(d => d + "/icons").concat([home + "/.icons"]);
|
||||
return dataDirs.filter((dir, index) => dir && dataDirs.indexOf(dir) === index);
|
||||
}
|
||||
|
||||
readonly property var _baseDirs: {
|
||||
const bases = [..._userIconRoots];
|
||||
for (const d of _dataDirs) {
|
||||
const icons = d + "/icons";
|
||||
if (!bases.includes(icons))
|
||||
bases.push(icons);
|
||||
}
|
||||
return bases;
|
||||
}
|
||||
|
||||
// Loose icon files (AppImage managers, legacy /usr/share/pixmaps installs) live outside any theme.
|
||||
readonly property var _looseDirs: {
|
||||
const dirs = _userIconRoots.concat(_dataDirs.map(d => d + "/pixmaps"));
|
||||
return dirs.filter((dir, index) => dirs.indexOf(dir) === index);
|
||||
}
|
||||
|
||||
onManagedThemeChanged: _rebuild()
|
||||
@@ -45,6 +71,14 @@ Singleton {
|
||||
_rebuild();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DesktopEntries
|
||||
|
||||
function onApplicationsChanged() {
|
||||
root._indexLooseIcons(() => root._invalidateMisses());
|
||||
}
|
||||
}
|
||||
|
||||
// "System Default" leaves Qt's lookup, which sees only hicolor unless a Qt platform theme or QS_ICON_THEME is configured.
|
||||
function _probeSystemTheme() {
|
||||
if (Quickshell.env("QS_ICON_THEME"))
|
||||
@@ -71,36 +105,76 @@ printf '%s' "$v" | tr -d "'\\""`;
|
||||
});
|
||||
}
|
||||
|
||||
function _invalidateMisses() {
|
||||
const c = {};
|
||||
let changed = false;
|
||||
for (const name in _cache) {
|
||||
if (_cache[name])
|
||||
c[name] = _cache[name];
|
||||
else
|
||||
changed = true;
|
||||
}
|
||||
if (!changed)
|
||||
return;
|
||||
_cacheGeneration++;
|
||||
_cache = c;
|
||||
_bumpRevision();
|
||||
}
|
||||
|
||||
function _indexLooseIcons(callback) {
|
||||
const generation = ++_looseIndexGeneration;
|
||||
const args = ["find", "-L"].concat(_looseDirs, ["-maxdepth", "1", "(", "-name", "*.svg", "-o", "-name", "*.png", "-o", "-name", "*.xpm", ")"]);
|
||||
Proc.runCommand("iconLooseIndex", args, (out, code) => {
|
||||
if (root._looseIndexGeneration !== generation)
|
||||
return;
|
||||
const index = {};
|
||||
const paths = (out || "").trim().split("\n").filter(s => s);
|
||||
for (const path of paths) {
|
||||
const fileName = path.substring(path.lastIndexOf("/") + 1);
|
||||
const name = fileName.replace(/\.(svg|png|xpm)$/, "");
|
||||
const current = index[name];
|
||||
index[name] = current ? root._pickBest([current, path]) : path;
|
||||
}
|
||||
root._looseIconPaths = index;
|
||||
if (callback)
|
||||
callback();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function _rebuild() {
|
||||
const rebuildGeneration = ++_rebuildGeneration;
|
||||
_cacheGeneration++;
|
||||
_cache = ({});
|
||||
_dirsForTheme = "";
|
||||
_indexLooseIcons(() => root._invalidateMisses());
|
||||
if (!managedTheme) {
|
||||
_searchDirs = [];
|
||||
_dirsForTheme = "";
|
||||
_bumpRevision();
|
||||
return;
|
||||
}
|
||||
const theme = managedTheme;
|
||||
const bases = _baseDirs.join(" ");
|
||||
const script = `BASES="${bases}"
|
||||
find_index() { for b in $BASES; do [ -f "$b/$1/index.theme" ] && { echo "$b/$1/index.theme"; return 0; }; done; return 1; }
|
||||
visited=""; queue="${theme}"; order=""
|
||||
const script = `theme=$1
|
||||
shift
|
||||
find_index() { target=$1; shift; for b; do [ -f "$b/$target/index.theme" ] && { echo "$b/$target/index.theme"; return 0; }; done; return 1; }
|
||||
visited=""; queue="$theme"; order=""
|
||||
while [ -n "$queue" ]; do
|
||||
cur=\${queue%% *}; rest=\${queue#"$cur"}; queue=\${rest# }
|
||||
[ -z "$cur" ] && continue
|
||||
case " $visited " in *" $cur "*) continue;; esac
|
||||
visited="$visited $cur"; order="$order $cur"
|
||||
idx=$(find_index "$cur") || continue
|
||||
inh=$(sed -n 's/^Inherits=//p' "$idx" | head -1 | tr -d '"' | tr ',' ' ')
|
||||
queue="$queue $inh"
|
||||
cur=\${queue%% *}; rest=\${queue#"$cur"}; queue=\${rest# }
|
||||
[ -z "$cur" ] && continue
|
||||
case " $visited " in *" $cur "*) continue;; esac
|
||||
visited="$visited $cur"; order="$order $cur"
|
||||
idx=$(find_index "$cur" "$@") || continue
|
||||
inh=$(sed -n 's/^Inherits=//p' "$idx" | head -1 | tr -d '"' | tr ',' ' ')
|
||||
queue="$queue $inh"
|
||||
done
|
||||
case " $visited " in *" hicolor "*) ;; *) order="$order hicolor";; esac
|
||||
for t in $order; do for b in $BASES; do d="$b/$t"; [ -d "$d" ] && echo "$d"; done; done`;
|
||||
for t in $order; do for b; do d="$b/$t"; [ -d "$d" ] && echo "$d"; done; done`;
|
||||
|
||||
Proc.runCommand("iconChain:" + theme, ["sh", "-c", script], (out, code) => {
|
||||
if (root.managedTheme !== theme)
|
||||
Proc.runCommand("iconChain:" + theme, ["sh", "-c", script, "icon-chain", theme].concat(_baseDirs), (out, code) => {
|
||||
if (root.managedTheme !== theme || root._rebuildGeneration !== rebuildGeneration)
|
||||
return;
|
||||
root._searchDirs = (out || "").trim().split("\n").filter(s => s);
|
||||
root._dirsForTheme = theme;
|
||||
root._cacheGeneration++;
|
||||
root._cache = ({});
|
||||
root._bumpRevision();
|
||||
});
|
||||
@@ -114,7 +188,7 @@ for t in $order; do for b in $BASES; do d="$b/$t"; [ -d "$d" ] && echo "$d"; don
|
||||
return "";
|
||||
if (!/^[\w.+-]+$/.test(name))
|
||||
return "";
|
||||
if (_dirsForTheme !== managedTheme || _searchDirs.length === 0)
|
||||
if (_dirsForTheme !== managedTheme)
|
||||
return "";
|
||||
if (name in _cache)
|
||||
return _cache[name] || "";
|
||||
@@ -124,15 +198,27 @@ for t in $order; do for b in $BASES; do d="$b/$t"; [ -d "$d" ] && echo "$d"; don
|
||||
}
|
||||
|
||||
function _resolveAsync(name) {
|
||||
const dirs = _searchDirs.join(" ");
|
||||
const script = `find -L ${dirs} \\( -name '${name}.svg' -o -name '${name}.png' \\) 2>/dev/null`;
|
||||
Proc.runCommand("iconResolve:" + name, ["sh", "-c", script], (out, code) => {
|
||||
const paths = (out || "").trim().split("\n").filter(s => s);
|
||||
const best = root._pickBest(paths);
|
||||
const generation = _cacheGeneration;
|
||||
|
||||
function finish(paths) {
|
||||
if (root._cacheGeneration !== generation)
|
||||
return;
|
||||
const best = paths.length > 0 ? root._pickBest(paths) : root._looseIconPaths[name] || "";
|
||||
const c = root._cache;
|
||||
c[name] = best ? Paths.toFileUrl(best) : "";
|
||||
root._cache = c;
|
||||
root._bumpRevision();
|
||||
}
|
||||
|
||||
if (_searchDirs.length === 0) {
|
||||
finish([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const args = ["find", "-L"].concat(_searchDirs, ["(", "-name", name + ".svg", "-o", "-name", name + ".png", ")"]);
|
||||
Proc.runCommand("iconResolveTheme:" + name, args, (out, code) => {
|
||||
const paths = (out || "").trim().split("\n").filter(s => s);
|
||||
finish(paths);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
@@ -154,7 +240,11 @@ for t in $order; do for b in $BASES; do d="$b/$t"; [ -d "$d" ] && echo "$d"; don
|
||||
if (path.startsWith(_searchDirs[i] + "/"))
|
||||
return i;
|
||||
}
|
||||
return _searchDirs.length;
|
||||
for (let i = 0; i < _looseDirs.length; i++) {
|
||||
if (path.startsWith(_looseDirs[i] + "/"))
|
||||
return _searchDirs.length + i;
|
||||
}
|
||||
return _searchDirs.length + _looseDirs.length;
|
||||
}
|
||||
|
||||
function _score(path) {
|
||||
@@ -166,7 +256,7 @@ for t in $order; do for b in $BASES; do d="$b/$t"; [ -d "$d" ] && echo "$d"; don
|
||||
else if (path.includes("/places/") || path.includes("/devices/") || path.includes("/mimetypes/") || path.includes("/status/") || path.includes("/actions/"))
|
||||
s += 100000000;
|
||||
|
||||
s += Math.max(0, (64 - _chainIndex(path))) * 1000000;
|
||||
s += Math.min(99, Math.max(0, _searchDirs.length + _looseDirs.length - _chainIndex(path))) * 1000000;
|
||||
|
||||
if (path.endsWith(".svg"))
|
||||
s += 100000;
|
||||
|
||||
Reference in New Issue
Block a user