summaryrefslogtreecommitdiffstats
path: root/quickshell/modules/notifications/NotificationFocus.qml
diff options
context:
space:
mode:
Diffstat (limited to 'quickshell/modules/notifications/NotificationFocus.qml')
-rw-r--r--quickshell/modules/notifications/NotificationFocus.qml239
1 files changed, 239 insertions, 0 deletions
diff --git a/quickshell/modules/notifications/NotificationFocus.qml b/quickshell/modules/notifications/NotificationFocus.qml
new file mode 100644
index 0000000..ce59b2a
--- /dev/null
+++ b/quickshell/modules/notifications/NotificationFocus.qml
@@ -0,0 +1,239 @@
+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"
+ ])
+ }
+}