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) { isAlreadyAdded = true break } if (!winApp.startsWith("steam_app_") && !pinId.startsWith("steam_app_") && pinId !== "steam") { if (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 getWindowsForApp(appId) { if (!appId) return []; let lower = appId.toLowerCase().trim() let matching = [] for (let i = 0; i < openWindowApps.length; i++) { let winObj = openWindowApps[i] if (!winObj) continue; let winApp = typeof winObj === "string" ? winObj.toLowerCase().trim() : (winObj.appId || "").toLowerCase().trim() let isMatch = false if (winApp === lower) { isMatch = true } else if (!winApp.startsWith("steam_app_") && !lower.startsWith("steam_app_") && lower !== "steam") { if (winApp.includes(lower) || lower.includes(winApp)) { isMatch = true } } if (isMatch) { matching.push({ id: typeof winObj === "object" ? (winObj.id || winApp) : winApp, appId: winApp, name: typeof winObj === "object" ? (winObj.name || winApp) : winApp, icon: typeof winObj === "object" ? (winObj.icon || winApp) : winApp, caption: typeof winObj === "object" ? (winObj.caption || winObj.name || winApp) : winApp, minimized: typeof winObj === "object" ? (winObj.minimized || false) : false, active: typeof winObj === "object" ? (winObj.active || false) : false }) } } return matching } function focusSpecificWindow(winObj, iconX, iconY) { if (!winObj) return; let query = (winObj.caption || winObj.appId || "").toLowerCase().trim() let ix = Math.round(iconX || 0) let iy = Math.round(iconY || 0) root.activeAppId = (winObj.appId || "").toLowerCase().trim() proc.running = false proc.command = ["python3", Quickshell.env("HOME") + "/.config/quickshell/services/python/kwin_focus.py", query, ix.toString(), iy.toString()] proc.running = true } 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) } } property real lastLaunchTime: 0 property string lastLaunchCmd: "" function launchApp(cmd) { if (!cmd) return; let cleanCmd = cmd.trim() if (cleanCmd === "") return; let now = Date.now() if (cleanCmd === lastLaunchCmd && (now - lastLaunchTime) < 600) { return; // Ignore rapid double-clicks within 600ms } lastLaunchTime = now lastLaunchCmd = cleanCmd let execCmd = cleanCmd.replace(/%[a-zA-Z]/g, "").trim() root.activeAppId = execCmd.toLowerCase() Quickshell.execDetached(["python3", Quickshell.env("HOME") + "/.config/quickshell/services/python/launch_app.py", cleanCmd]) } 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 } }