import QtQuick import Quickshell import Quickshell.Io Item { id: root property string pendingApp: "" function normalize(value) { if (!value) return "" var result = String(value).toLowerCase().trim() result = result.replace(/^org\./, "") result = result.replace(/^com\./, "") result = result.replace(/^net\./, "") result = result.replace(/^app\./, "") result = result.replace(/\.desktop$/, "") result = result.replace(/\.desktop$/i, "") result = result.replace(/[^a-z0-9]+/g, "") return result } function candidates(value) { var result = [] var raw = String(value || "").toLowerCase().trim() function add(candidate) { if (!candidate) return candidate = normalize(candidate) if (candidate && result.indexOf(candidate) === -1) result.push(candidate) } add(raw) if (raw.indexOf(".") !== -1) { var parts = raw.split(".") add(parts[parts.length - 1]) for (var i = 0; i < parts.length; i++) add(parts[i]) } if (raw.indexOf("/") !== -1) add(raw.split("/").pop()) if (raw.indexOf("-") !== -1) { var dashParts = raw.split("-") for (var j = 0; j < dashParts.length; j++) add(dashParts[j]) } if (raw.indexOf("_") !== -1) { var underscoreParts = raw.split("_") for (var k = 0; k < underscoreParts.length; k++) add(underscoreParts[k]) } return result } function matchScore(notificationApp, windowAppId, windowTitle) { var notificationCandidates = candidates(notificationApp) var windowCandidates = candidates(windowAppId) var best = 0 for (var i = 0; i < notificationCandidates.length; i++) { var notificationCandidate = notificationCandidates[i] for (var j = 0; j < windowCandidates.length; j++) { var windowCandidate = windowCandidates[j] if (notificationCandidate === windowCandidate) best = Math.max(best, 100) else if ( notificationCandidate.length >= 4 && windowCandidate.length >= 4 && ( notificationCandidate.indexOf(windowCandidate) !== -1 || windowCandidate.indexOf(notificationCandidate) !== -1 ) ) { best = Math.max(best, 60) } } } var titleCandidates = candidates(windowTitle) for (var n = 0; n < notificationCandidates.length; n++) { for (var t = 0; t < titleCandidates.length; t++) { if ( notificationCandidates[n].length >= 4 && notificationCandidates[n] === titleCandidates[t] ) { best = Math.max(best, 30) } } } return best } Process { id: windowsProcess command: ["niri", "msg", "-j", "windows"] stdout: StdioCollector { id: windowsOutput onStreamFinished: { var windows try { windows = JSON.parse(this.text) } catch (error) { console.error( "Failed to parse Niri windows:", error ) root.pendingApp = "" return } var notificationApp = root.pendingApp root.pendingApp = "" if (!notificationApp) { console.warn("Notification focus: empty app name") return } if (!Array.isArray(windows)) { console.error( "Niri windows response is not an array" ) return } var bestWindow = null var bestScore = 0 for (var i = 0; i < windows.length; i++) { var window = windows[i] if (!window || window.id === undefined) continue var appId = window.app_id || "" var title = window.title || "" var score = matchScore( notificationApp, appId, title ) if (score > bestScore) { bestScore = score bestWindow = window } } if (!bestWindow) { console.log( "No matching Niri window found:", notificationApp ) return } console.log( "FOCUSING WINDOW:", bestWindow.id, "notification app:", notificationApp, "app_id:", bestWindow.app_id, "title:", bestWindow.title, "score:", bestScore ) focusProcess.exec([ "niri", "msg", "action", "focus-window", "--id", String(bestWindow.id) ]) } } } Process { id: focusProcess } function focusSource(notification) { if (!notification) return var app = notification.appName || "" if (!app) { console.warn( "Notification has no application name" ) return } root.pendingApp = app windowsProcess.exec([ "niri", "msg", "-j", "windows" ]) } }