diff options
Diffstat (limited to 'home-configuration/config/quickshell/modules/notifications')
3 files changed, 505 insertions, 0 deletions
diff --git a/home-configuration/config/quickshell/modules/notifications/NotificationCard.qml b/home-configuration/config/quickshell/modules/notifications/NotificationCard.qml new file mode 100644 index 0000000..983cbd7 --- /dev/null +++ b/home-configuration/config/quickshell/modules/notifications/NotificationCard.qml @@ -0,0 +1,257 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell.Services.Notifications +import "../../config.js" as Config + +Rectangle { + id: root + + required property var notification + + signal activated() + + property bool dismissing: false + + Layout.fillWidth: true + + implicitHeight: body.implicitHeight + 20 + + radius: Config.blockRadius + + color: Config.barBackground + + border.width: 1 + border.color: Config.notificationBorder + + x: dismissing ? Config.notificationSlideDistance : 0 + opacity: dismissing ? 0 : 1 + + Component.onCompleted: { + x = Config.notificationSlideDistance + opacity = 0 + + enterAnimation.start() + + if (notification.urgency !== NotificationUrgency.Critical) + dismissTimer.start() + } + + Timer { + id: dismissTimer + + interval: Config.notificationTimeout + repeat: false + + onTriggered: { + dismiss() + } + } + + function dismiss() { + if (dismissing) + return + + dismissTimer.stop() + dismissing = true + exitAnimation.start() + } + + SequentialAnimation { + id: enterAnimation + + ParallelAnimation { + NumberAnimation { + target: root + property: "x" + from: Config.notificationSlideDistance + to: 0 + duration: Config.notificationAnimationDuration + easing.type: Easing.OutCubic + } + + NumberAnimation { + target: root + property: "opacity" + from: 0 + to: 1 + duration: Config.notificationAnimationDuration + easing.type: Easing.OutCubic + } + } + } + + SequentialAnimation { + id: exitAnimation + + ParallelAnimation { + NumberAnimation { + target: root + property: "x" + to: Config.notificationSlideDistance + duration: Config.notificationAnimationDuration + easing.type: Easing.InCubic + } + + NumberAnimation { + target: root + property: "opacity" + to: 0 + duration: Config.notificationAnimationDuration + easing.type: Easing.InCubic + } + } + + ScriptAction { + script: root.notification.expire() + } + } + + ColumnLayout { + id: body + + anchors.fill: parent + anchors.margins: 10 + + spacing: 6 + + RowLayout { + Layout.fillWidth: true + + Image { + visible: root.notification.appIcon !== "" + + source: root.notification.appIcon + + sourceSize.width: 24 + sourceSize.height: 24 + + Layout.preferredWidth: 24 + Layout.preferredHeight: 24 + + fillMode: Image.PreserveAspectFit + asynchronous: true + } + + Text { + Layout.fillWidth: true + + text: root.notification.appName || "Notification" + + color: Config.textPrimary + + font.family: Config.fontFamily + font.pixelSize: 12 + font.bold: true + + elide: Text.ElideRight + } + + Text { + text: "×" + + color: "#6c7086" + + font.family: Config.fontFamily + font.pixelSize: 18 + font.bold: true + + MouseArea { + anchors.fill: parent + + cursorShape: Qt.PointingHandCursor + + onClicked: { + root.dismiss() + } + } + } + } + + Text { + Layout.fillWidth: true + + text: root.notification.summary + + color: Config.textPrimary + + font.family: Config.fontFamily + font.pixelSize: Config.fontSize + font.bold: true + + wrapMode: Text.Wrap + } + + Text { + visible: root.notification.body !== "" + + Layout.fillWidth: true + + text: root.notification.body + + color: Config.textSecondary + + font.family: Config.fontFamily + font.pixelSize: 12 + + wrapMode: Text.Wrap + + maximumLineCount: 4 + elide: Text.ElideRight + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 3 + + radius: 2 + + gradient: Gradient { + GradientStop { + position: 0.0 + color: Config.accentGreen + } + + GradientStop { + position: 0.33 + color: Config.accentYellow + } + + GradientStop { + position: 0.66 + color: Config.accentOrange + } + + GradientStop { + position: 1.0 + color: Config.accentRed + } + } + } + } + + MouseArea { + anchors.fill: parent + + cursorShape: Qt.PointingHandCursor + + onClicked: { + root.activated() + } + } + + MouseArea { + anchors.right: parent.right + anchors.top: parent.top + + width: 36 + height: 36 + + z: 10 + + cursorShape: Qt.PointingHandCursor + + onClicked: { + root.dismiss() + } + } +} diff --git a/home-configuration/config/quickshell/modules/notifications/NotificationFocus.qml b/home-configuration/config/quickshell/modules/notifications/NotificationFocus.qml new file mode 100644 index 0000000..af52eaa --- /dev/null +++ b/home-configuration/config/quickshell/modules/notifications/NotificationFocus.qml @@ -0,0 +1,123 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import "../../config.js" as Config + +Item { + id: root + + property string pendingApp: "" + + 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 app = root.pendingApp.toLowerCase() + root.pendingApp = "" + + if (Config.notificationVesktopNames.indexOf(app) !== -1) { + for (var i = 0; i < windows.length; i++) { + var niriWindow = windows[i] + var appId = (niriWindow.app_id || "").toLowerCase() + + if (appId === Config.notificationVesktopAppId) { + console.log( + "FOCUSING VESKTOP WINDOW:", + niriWindow.id, + niriWindow.title + ) + + focusProcess.exec([ + "niri", + "msg", + "action", + "focus-window", + "--id", + String(niriWindow.id) + ]) + + return + } + } + + console.log( + "No matching Niri window found for:", + app + ) + + return + } + + for (var j = 0; j < windows.length; j++) { + var window = windows[j] + var windowAppId = (window.app_id || "").toLowerCase() + + if ( + windowAppId === app || + windowAppId === app + ".desktop" + ) { + console.log( + "FOCUSING WINDOW:", + window.id, + window.app_id, + window.title + ) + + focusProcess.exec([ + "niri", + "msg", + "action", + "focus-window", + "--id", + String(window.id) + ]) + + return + } + } + + console.log( + "No matching Niri window found for:", + app + ) + } + } + } + + Process { + id: focusProcess + } + + function focusSource(notification) { + if (!notification) + return + + root.pendingApp = notification.appName + + windowsProcess.exec([ + "niri", + "msg", + "-j", + "windows" + ]) + } +} diff --git a/home-configuration/config/quickshell/modules/notifications/Notifications.qml b/home-configuration/config/quickshell/modules/notifications/Notifications.qml new file mode 100644 index 0000000..2965b90 --- /dev/null +++ b/home-configuration/config/quickshell/modules/notifications/Notifications.qml @@ -0,0 +1,125 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import Quickshell.Services.Notifications +import "../../config.js" as Config +import "./" + +Item { + id: root + + NotificationServer { + id: server + + bodySupported: true + imageSupported: true + actionsSupported: true + actionIconsSupported: true + bodyMarkupSupported: true + + onNotification: function(notification) { + console.log("NOTIFICATION") + console.log(" app:", notification.appName) + console.log(" summary:", notification.summary) + console.log(" body:", notification.body) + console.log(" actions:", notification.actions.length) + + for (var i = 0; i < notification.actions.length; i++) { + console.log( + " action:", + notification.actions[i].identifier, + notification.actions[i].text + ) + } + + notification.tracked = true + } + } + + NotificationFocus { + id: focus + } + + function activateNotification(notification) { + if (!notification) + return + + console.log( + "ACTIVATE:", + notification.appName, + notification.summary + ) + + focus.focusSource(notification) + + for (var i = 0; i < notification.actions.length; i++) { + var action = notification.actions[i] + + if (action.identifier === "default") { + console.log( + "Invoking default action:", + action.text + ) + + action.invoke() + return + } + } + + console.log("No default action available") + notification.dismiss() + } + + PanelWindow { + id: window + + screen: Quickshell.screens.find(function(screen) { + return screen.name === Config.barOutput + }) + + anchors { + top: true + right: true + } + + margins { + top: Config.notificationMarginTop + right: Config.notificationMarginRight + } + + exclusionMode: ExclusionMode.Ignore + + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.namespace: "notifications" + + color: "transparent" + + implicitWidth: Config.notificationWidth + implicitHeight: list.implicitHeight + + ColumnLayout { + id: list + + anchors.top: parent.top + anchors.right: parent.right + + width: Config.notificationWidth + spacing: Config.notificationSpacing + + Repeater { + model: server.trackedNotifications + + delegate: NotificationCard { + required property var modelData + + notification: modelData + + onActivated: { + root.activateNotification(modelData) + } + } + } + } + } +} |
