pragma Singleton import QtQuick import Quickshell import Quickshell.Io import Qt.labs.platform // ← gives us StandardPaths Singleton { id: root /* ──────────────── basic state ──────────────── */ signal colorsUpdated() readonly property string _homeUrl: StandardPaths.writableLocation(StandardPaths.HomeLocation) readonly property string homeDir: _homeUrl.startsWith("file://") ? _homeUrl.substring(7) : _homeUrl readonly property string wallpaperPath: homeDir + "/quickshell/current_wallpaper" readonly property string notifyPath: homeDir + "/quickshell/wallpaper_changed" property bool matugenAvailable: false property string matugenJson: "" property var matugenColors: ({}) property bool extractionRequested: false Component.onCompleted: { console.log("Colors.qml → home =", homeDir) // Don't automatically run color extraction - only when requested matugenCheck.running = true // Just check if matugen is available // Start monitoring for wallpaper changes wallpaperMonitorTimer.start() } /* ──────────────── availability checks ──────────────── */ Process { id: matugenCheck command: ["which", "matugen"] onExited: (code) => { matugenAvailable = (code === 0) console.log("Matugen in PATH:", matugenAvailable) if (!matugenAvailable) { console.warn("Matugen missing → dynamic theme disabled") Theme.rootObj.wallpaperErrorStatus = "matugen_missing" return } // If extraction was requested, continue the process if (extractionRequested) { console.log("Continuing with color extraction") fileChecker.running = true } } } Process { id: fileChecker // exists & readable? command: ["test", "-r", wallpaperPath] onExited: (code) => { if (code === 0) { matugenProcess.running = true } else { console.error("code", code) console.error("Wallpaper not found:", wallpaperPath) Theme.rootObj.showWallpaperError() } } } /* ──────────────── matugen invocation ──────────────── */ Process { id: matugenProcess command: ["matugen", "-v", "image", wallpaperPath, "--json", "hex"] /* ── grab stdout as a stream ── */ stdout: StdioCollector { id: matugenCollector onStreamFinished: { const out = matugenCollector.text if (!out.length) { console.error("matugen produced zero bytes\nstderr:", matugenProcess.stderr) Theme.rootObj.showWallpaperError() return } try { root.matugenJson = out root.matugenColors = JSON.parse(out) root.colorsUpdated() Theme.rootObj.wallpaperErrorStatus = "" } catch (e) { console.error("JSON parse failed:", e) Theme.rootObj.showWallpaperError() } } } /* grab stderr too, so we can print it above */ stderr: StdioCollector { id: matugenErr } } /* ──────────────── wallpaper change monitor ──────────────── */ property string lastWallpaperTimestamp: "" Timer { id: wallpaperMonitorTimer interval: 1000 // Check every second repeat: true onTriggered: { wallpaperNotifyMonitor.reload() } } FileView { id: wallpaperNotifyMonitor path: "file://" + notifyPath onLoaded: { var timestamp = wallpaperNotifyMonitor.text() if (timestamp && timestamp !== lastWallpaperTimestamp) { console.log("Wallpaper change detected - updating dynamic theme") lastWallpaperTimestamp = timestamp // Only update if we're currently using dynamic theme if (typeof Theme !== "undefined" && Theme.isDynamicTheme) { console.log("Triggering color extraction due to wallpaper change") extractColors() } } } onLoadFailed: { // File doesn't exist yet, this is normal } } /* ──────────────── public helper ──────────────── */ function extractColors() { console.log("Colors.extractColors() called, matugenAvailable:", matugenAvailable) extractionRequested = true if (matugenAvailable) fileChecker.running = true else matugenCheck.running = true } function getMatugenColor(path, fallback) { let cur = matugenColors?.colors?.dark for (const part of path.split(".")) { if (!cur || typeof cur !== "object" || !(part in cur)) return fallback cur = cur[part] } return cur || fallback } /* ──────────────── color properties (MD3) ──────────────── */ property color primary: getMatugenColor("primary", "#42a5f5") property color secondary: getMatugenColor("secondary", "#8ab4f8") property color tertiary: getMatugenColor("tertiary", "#bb86fc") property color tertiaryContainer: getMatugenColor("tertiary_container", "#3700b3") property color error: getMatugenColor("error", "#cf6679") property color inversePrimary: getMatugenColor("inverse_primary", "#6200ea") /* backgrounds */ property color bg: getMatugenColor("background", "#1a1c1e") property color surface: getMatugenColor("surface", "#1a1c1e") property color surfaceContainer: getMatugenColor("surface_container", "#1e2023") property color surfaceContainerHigh: getMatugenColor("surface_container_high", "#292b2f") property color surfaceVariant: getMatugenColor("surface_variant", "#44464f") /* text */ property color surfaceText: getMatugenColor("on_background", "#e3e8ef") property color primaryText: getMatugenColor("on_primary", "#ffffff") property color surfaceVariantText: getMatugenColor("on_surface_variant", "#c4c7c5") /* containers & misc */ property color primaryContainer: getMatugenColor("primary_container", "#1976d2") property color surfaceTint: getMatugenColor("surface_tint", "#8ab4f8") property color outline: getMatugenColor("outline", "#8e918f") /* legacy aliases */ property color accentHi: primary property color accentLo: secondary function isColorDark(c) { return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5 } }