pragma Singleton import QtQuick import Quickshell import Quickshell.Io import Quickshell.Wayland Item { id: root // Pinned Application Defaults (Loaded dynamically from disk on startup) property var pinnedApps: [ { appId: "alacritty", name: "Alacritty", icon: "Alacritty", cmd: "alacritty" }, { appId: "dolphin", name: "Dolphin", icon: "org.kde.dolphin", cmd: "dolphin" }, { appId: "firefox", name: "Firefox", icon: "firefox", cmd: "firefox" }, { appId: "code", name: "VS Code", icon: "com.visualstudio.code", cmd: "code" }, { appId: "kate", name: "Kate", icon: "kate", cmd: "kate" } ] property var runningWindows: [] property var openWindowApps: [] property bool isFullscreen: false property string activeAppId: "" property string activeWindowTitle: "" // Dynamic Unified Dock Model (Pinned Apps + Unpinned REAL Desktop Open Windows ONLY) readonly property var allDockApps: { let list = [] // 1. Pinned Apps (Always present) for (let i = 0; i < pinnedApps.length; i++) { let p = pinnedApps[i] list.push({ appId: p.appId, name: p.name || p.appId, icon: p.icon || p.appId, cmd: p.cmd || p.appId, isPinned: true }) } // 2. Unpinned Apps (ONLY if they have a real open desktop window reported by KWin!) let addedAppIds = [] for (let i = 0; i < pinnedApps.length; i++) { addedAppIds.push(pinnedApps[i].appId.toLowerCase()) } for (let w = 0; w < openWindowApps.length; w++) { let winObj = openWindowApps[w] if (!winObj) continue; let winApp = typeof winObj === "string" ? winObj.toLowerCase().trim() : (winObj.appId || "").toLowerCase().trim() if (!winApp || winApp === "") continue; let isAlreadyAdded = false for (let k = 0; k < addedAppIds.length; k++) { let pinId = addedAppIds[k] if (winApp === pinId || winApp.includes(pinId) || pinId.includes(winApp)) { isAlreadyAdded = true break } } if (!isAlreadyAdded) { addedAppIds.push(winApp) let name = typeof winObj === "object" ? (winObj.name || winApp) : winApp let icon = typeof winObj === "object" ? (winObj.icon || winApp) : winApp list.push({ appId: winApp, name: name, icon: icon, cmd: winApp, isPinned: false }) } } return list } function isPinned(appId) { if (!appId) return false; let lower = appId.toLowerCase() for (let i = 0; i < pinnedApps.length; i++) { if (pinnedApps[i].appId.toLowerCase() === lower || lower.includes(pinnedApps[i].appId.toLowerCase())) { return true } } return false } function isRunning(appId) { if (!appId) return false; let lower = appId.toLowerCase().trim() for (let i = 0; i < openWindowApps.length; i++) { let winObj = openWindowApps[i] let winApp = typeof winObj === "string" ? winObj.toLowerCase().trim() : (winObj.appId || "").toLowerCase().trim() if (winApp === lower || winApp.includes(lower) || lower.includes(winApp)) { return true } } return false } function isActive(appId) { if (!appId) return false; let lower = appId.toLowerCase().trim() if (!isRunning(lower)) return false; if (activeAppId && activeAppId !== "") { let actLower = activeAppId.toLowerCase().trim() if (actLower === lower || actLower.includes(lower) || lower.includes(actLower)) { return true } } return false } function focusApp(appId, cmd, iconX, iconY) { if (!appId) return; let lower = appId.toLowerCase().trim() let ix = Math.round(iconX || 0) let iy = Math.round(iconY || 0) if (isRunning(appId)) { let currentlyActive = isActive(appId) if (currentlyActive) { root.activeAppId = "" } else { root.activeAppId = lower } proc.running = false proc.command = ["python3", Quickshell.env("HOME") + "/.config/quickshell/services/python/kwin_focus.py", lower, ix.toString(), iy.toString()] proc.running = true } else if (cmd || appId) { launchApp(cmd || appId) } } function launchApp(cmd) { if (!cmd) return; let cleanCmd = cmd.trim() if (cleanCmd === "") return; let execCmd = cleanCmd.replace(/%[a-zA-Z]/g, "").trim() root.activeAppId = execCmd.toLowerCase() if (execCmd.startsWith("flatpak run ") || execCmd.includes("flatpak ")) { Quickshell.execDetached(["sh", "-c", execCmd + " &"]) return; } let baseName = execCmd.replace(/\.desktop$/, "").trim() // Build universal Flatpak + desktop launcher + native binary fallback chain let shellCmd = "" if (baseName.startsWith("org.") || baseName.startsWith("io.") || baseName.startsWith("com.") || baseName.startsWith("app.")) { let shortName = baseName.split(".").pop() shellCmd = "flatpak run " + baseName + " 2>/dev/null || flatpak run " + shortName + " 2>/dev/null || gtk-launch " + baseName + " 2>/dev/null || gtk-launch " + shortName + " 2>/dev/null || " + shortName + " 2>/dev/null || " + execCmd + " &" } else { // Known reverse domain mappings for common Flatpak apps like spotify, feishin, zen, vesktop, discord, etc. let flatpakMapping = { "spotify": "com.spotify.Client", "feishin": "io.github.jeffvli.feishin", "zen": "io.github.zen_browser.zen", "zen-browser": "io.github.zen_browser.zen", "vesktop": "dev.vencord.Vesktop", "discord": "com.discordapp.Discord", "code": "com.visualstudio.code", "vscodium": "com.vscodium.codium", "obs": "com.obsproject.Studio", "steam": "com.valvesoftware.Steam", "heroic": "com.heroicgameslauncher.hgl" } let mappedFlatpak = flatpakMapping[baseName.toLowerCase()] || "" if (mappedFlatpak !== "") { shellCmd = "flatpak run " + mappedFlatpak + " 2>/dev/null || flatpak run " + baseName + " 2>/dev/null || gtk-launch " + mappedFlatpak + " 2>/dev/null || gtk-launch " + baseName + " 2>/dev/null || " + execCmd + " &" } else { shellCmd = "flatpak run " + baseName + " 2>/dev/null || gtk-launch " + baseName + " 2>/dev/null || " + execCmd + " &" } } Quickshell.execDetached(["sh", "-c", shellCmd]) } function closeApp(appId) { if (appId) { let lower = appId.toLowerCase().trim() if (isActive(appId)) root.activeAppId = "" proc.running = false proc.command = ["bash", "-c", "pkill -i -f " + appId + " 2>/dev/null &"] proc.running = true } } function toggleLauncher() { PopupService.toggleAppLauncher() } function togglePin(app) { if (!app || !app.appId) return; let existsIndex = -1 let lower = app.appId.toLowerCase() for (let i = 0; i < pinnedApps.length; i++) { if (pinnedApps[i].appId.toLowerCase() === lower || lower.includes(pinnedApps[i].appId.toLowerCase())) { existsIndex = i break } } let list = Array.from(pinnedApps) if (existsIndex >= 0) { list.splice(existsIndex, 1) } else { list.push({ appId: app.appId, name: app.name || app.appId, icon: app.icon || app.appId, cmd: app.cmd || app.appId }) } pinnedApps = list savePinnedApps() } function reorderPinnedApps(fromIndex, toIndex) { if (fromIndex < 0 || fromIndex >= pinnedApps.length || toIndex < 0 || toIndex >= pinnedApps.length || fromIndex === toIndex) return; let list = Array.from(pinnedApps) let item = list.splice(fromIndex, 1)[0] list.splice(toIndex, 0, item) pinnedApps = list savePinnedApps() } function savePinnedApps() { let jsonStr = JSON.stringify(pinnedApps) saveProc.command = ["python3", Quickshell.env("HOME") + "/.config/quickshell/services/python/save_pinned_apps.py", jsonStr] saveProc.running = true } function updateIconGeometries(map) { if (!map || typeof map !== "object") return; let jsonStr = JSON.stringify(map) updateGeomProc.command = ["python3", Quickshell.env("HOME") + "/.config/quickshell/services/python/update_icon_geometries.py", jsonStr] updateGeomProc.running = true } Process { id: proc } Process { id: saveProc } Process { id: updateGeomProc } // Read saved pinned apps from JSON as a SINGLE LINE stream on startup Process { id: readPinnedProc command: ["python3", Quickshell.env("HOME") + "/.config/quickshell/services/python/read_pinned_apps.py"] running: true stdout: SplitParser { onRead: data => { let txt = data.trim() if (txt === "DEFAULT" || txt === "") return; try { let parsed = JSON.parse(txt) if (parsed && Array.isArray(parsed)) { root.pinnedApps = parsed } } catch (e) {} } } } // Event-driven Active Window & Open Windows DBus Daemon Process { id: activeDaemon command: ["python3", "-u", Quickshell.env("HOME") + "/.config/quickshell/services/python/active_window_service.py"] running: true stdout: SplitParser { onRead: data => { let txt = data.trim() if (txt === "TOGGLE_LAUNCHER") { PopupService.toggleAppLauncher() } } } } // Single-line JSON State Reader Process (100ms) Process { id: readStateProc command: ["python3", Quickshell.env("HOME") + "/.config/quickshell/services/python/read_window_state.py"] stdout: SplitParser { onRead: data => { try { let parsed = JSON.parse(data.trim()) if (parsed) { if (parsed.active && parsed.active !== "") { root.activeAppId = parsed.active } if (parsed.open && Array.isArray(parsed.open)) { root.openWindowApps = parsed.open } if (parsed.fullscreen !== undefined) { root.isFullscreen = parsed.fullscreen } } } catch (e) {} } } } // 100ms High-speed State Reader Timer Timer { interval: 100 running: true repeat: true onTriggered: { if (!readStateProc.running) { readStateProc.running = true } } } Component.onCompleted: { if (!readPinnedProc.running) readPinnedProc.running = true if (!activeDaemon.running) activeDaemon.running = true if (!readStateProc.running) readStateProc.running = true } }