Files
DankMaterialShell/quickshell/Services/IconThemeService.qml
T
bbedward 4eb163538c media: change player style
theme: restore layer outline option
launcher/theme: tweaks and fix icon service
2026-09-22 09:00:34 -04:00

230 lines
7.6 KiB
QML

pragma Singleton
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import "../Common/GSettings.js" as GSettings // qmllint disable unused-imports
import "IconThemeIndex.js" as IconIndex
Singleton {
id: root
readonly property var log: Log.scoped("IconThemeService")
readonly property string settingsTheme: {
if (typeof SettingsData === "undefined")
return "";
const theme = SettingsData.resolveIconTheme();
return (!theme || theme === "System Default") ? "" : theme;
}
property string systemProbedTheme: ""
readonly property string managedTheme: settingsTheme || systemProbedTheme
property bool ready: false
property var _iconPaths: Object.create(null)
property bool _initialized: false
property bool _probing: false
property bool _building: false
property bool _rebuildPending: false
readonly property var _userIconRoots: {
const localData = Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation));
const home = Paths.strip(StandardPaths.writableLocation(StandardPaths.HomeLocation));
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.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;
}
readonly property var _looseDirs: {
const dirs = _userIconRoots.concat(_dataDirs.map(d => d + "/pixmaps"));
return dirs.filter((dir, index) => dirs.indexOf(dir) === index);
}
onSettingsThemeChanged: {
if (_initialized && !settingsTheme)
_probeSystemTheme();
}
onManagedThemeChanged: Qt.callLater(_rebuild)
Component.onCompleted: {
_initialized = true;
Paths.iconResolver = name => resolve(name);
_probeSystemTheme();
Qt.callLater(_rebuild);
}
Connections {
target: DesktopEntries
function onApplicationsChanged() {
if (!root.ready)
return;
Qt.callLater(root._rebuild);
}
}
function _probeSystemTheme() {
if (Quickshell.env("QS_ICON_THEME") || _probing)
return;
_probing = true;
const script = `v=$(sed -n 's/^gtk-icon-theme-name *= *//p' "\${XDG_CONFIG_HOME:-$HOME/.config}/gtk-3.0/settings.ini" 2>/dev/null | head -1)
[ -z "$v" ] && v=$(${GSettings.getCmd("org.gnome.desktop.interface", "icon-theme")})
printf '%s' "$v" | tr -d "'\\""`;
Proc.runCommand(null, ["sh", "-c", script], (out, code) => {
root.systemProbedTheme = (out || "").trim();
root._probing = false;
Qt.callLater(root._rebuild);
}, 0);
}
function _rebuild() {
if (!_initialized || _probing)
return;
if (_building) {
_rebuildPending = true;
return;
}
_building = true;
_rebuildPending = false;
const theme = managedTheme;
if (!theme) {
_publishIndex(theme, Object.create(null));
return;
}
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"
done
case " $visited " in *" hicolor "*) ;; *) order="$order hicolor";; esac
for t in $order; do for b; do d="$b/$t"; [ -d "$d" ] && echo "$d"; done; done`;
Proc.runCommand(null, ["sh", "-c", script, "icon-chain", theme].concat(_baseDirs), (out, code) => {
const dirs = (out || "").trim().split("\n").filter(dir => dir);
root._indexTheme(theme, dirs);
}, 0);
}
function _indexTheme(theme, dirs) {
indexJob.createObject(root, {
theme: theme,
searchDirs: dirs,
looseDirs: _looseDirs
});
}
function _publishIndex(theme, paths) {
_building = false;
if (managedTheme === theme && !_probing) {
_iconPaths = paths;
ready = true;
}
if (_rebuildPending)
Qt.callLater(_rebuild);
}
Component {
id: indexJob
Scope {
id: job
required property string theme
required property var searchDirs
required property var looseDirs
property var themeIndex: IconIndex.create()
property var looseIndex: IconIndex.create()
property int remaining: searchDirs.length ? 2 : 1
property bool finished: false
function complete() {
if (--remaining > 0)
return;
publish();
}
function publish() {
if (finished)
return;
finished = true;
for (const name in looseIndex.paths) {
if (!themeIndex.paths[name])
themeIndex.paths[name] = looseIndex.paths[name];
}
root._publishIndex(theme, themeIndex.paths);
destroy();
}
Process {
command: ["find", "-L"].concat(job.searchDirs, ["(", "-name", "*.svg", "-o", "-name", "*.png", ")"])
running: job.searchDirs.length > 0
stdout: SplitParser {
onRead: data => {
if (!job.finished)
IconIndex.add(job.themeIndex, data, job.searchDirs, job.looseDirs);
}
}
onExited: job.complete()
}
Process {
command: ["find", "-L"].concat(job.looseDirs, ["-maxdepth", "1", "(", "-name", "*.svg", "-o", "-name", "*.png", "-o", "-name", "*.xpm", ")"])
running: true
stdout: SplitParser {
onRead: data => {
if (!job.finished)
IconIndex.add(job.looseIndex, data, job.searchDirs, job.looseDirs);
}
}
onExited: job.complete()
}
Timer {
interval: 10000
running: true
onTriggered: {
root.log.warn("Icon theme index timed out:", job.theme);
job.publish();
}
}
}
}
function resolve(name) {
if (!name || !/^[\w.+-]+$/.test(name))
return "";
const path = _iconPaths[name];
return path ? Paths.toFileUrl(path) : "";
}
}