feat(notifications): implement notification service

This commit is contained in:
tux
2026-08-23 16:59:43 +05:30
parent 2b93af1a08
commit 1ded80d1c0
5 changed files with 259 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="style=bulk">
<g id="notification-box-line">
<path id="Subtract" fill-rule="evenodd" clip-rule="evenodd" d="M8.75 1.99994C5.02208 1.99994 2 5.02201 2 8.74994L2 15.2577C2 18.9857 5.02208 22.0077 8.75 22.0077L15.2578 22.0077C18.9857 22.0077 22.0078 18.9857 22.0078 15.2577L22.0078 8.74994C22.0078 8.09759 21.9153 7.46685 21.7426 6.87014C21.2141 8.15089 19.9531 9.05228 18.4816 9.05228C16.5342 9.05228 14.9554 7.47356 14.9554 5.52611C14.9554 4.05459 15.8568 2.79359 17.1376 2.26514C16.5409 2.09248 15.9101 1.99994 15.2578 1.99994L8.75 1.99994Z" fill="#BFBFBF"/>
<path id="vector (Stroke)" fill-rule="evenodd" clip-rule="evenodd" d="M8.75 15.5001C8.75 15.0858 9.08579 14.7501 9.5 14.7501H14.5C14.9142 14.7501 15.25 15.0858 15.25 15.5001C15.25 15.9143 14.9142 16.2501 14.5 16.2501H9.5C9.08579 16.2501 8.75 15.9143 8.75 15.5001Z" fill="#000000"/>
<circle id="vector" cx="18.4816" cy="5.52623" r="2.77617" fill="#000000"/>
</g>
</g>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,117 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Notifications
import qs.config
Rectangle {
id: root
property Notification notif
readonly property bool critical: notif?.urgency === NotificationUrgency.Critical
readonly property int timeoutDuration: {
const seconds = Math.round((notif?.expireTimeout ?? 0) / 1000);
return seconds > 0 ? seconds : 5;
}
readonly property string iconName: {
if (!notif)
return "";
const hints = notif.hints ?? {};
const hinted = String(hints["image-path"] ?? hints["image_path"] ?? hints["icon_path"] ?? "");
return notif.appIcon || notif.desktopEntry || hinted;
}
readonly property url themedIconUrl: iconName !== "" ? Quickshell.iconPath(iconName, false) : ""
readonly property url iconUrl: `${Quickshell.shellPath("assets")}/icons/notification.svg`
implicitWidth: 360
implicitHeight: content.implicitHeight + Appearance.padding * 2
radius: Appearance.radius
color: Appearance.colors.background
border.width: critical ? 1 : 0
border.color: Appearance.colors.accent
Timer {
id: countdown
interval: root.timeoutDuration * 1000
repeat: false
running: !root.critical && !hovered.hovered && visible
onTriggered: root.notif.expire()
}
HoverHandler {
id: hovered
}
RowLayout {
id: content
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Appearance.padding
spacing: Appearance.spacing
Item {
width: 40
height: 40
Layout.alignment: Qt.AlignTop
Image {
anchors.fill: parent
source: root.iconUrl
fillMode: Image.PreserveAspectFit
smooth: true
}
Image {
anchors.fill: parent
source: root.themedIconUrl
visible: root.themedIconUrl !== "" && status === Image.Ready
fillMode: Image.PreserveAspectFit
smooth: true
}
}
ColumnLayout {
spacing: 2
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
Text {
text: root.notif?.appName ?? ""
color: Appearance.colors.accent
font.family: Appearance.font.family
font.pointSize: Appearance.font.pointSize - 2
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
text: root.notif?.summary ?? ""
color: Appearance.colors.foreground
font.family: Appearance.font.family
font.pointSize: Appearance.font.pointSize
font.weight: Font.DemiBold
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
text: root.notif?.body ?? ""
visible: text !== ""
color: Appearance.colors.foreground
opacity: 0.8
font.family: Appearance.font.family
font.pointSize: Appearance.font.pointSize - 1
wrapMode: Text.Wrap
maximumLineCount: 3
elide: Text.ElideRight
textFormat: Text.PlainText
Layout.fillWidth: true
}
}
}
}

View File

@@ -0,0 +1,74 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Services.Notifications
Singleton {
id: root
// Notifications currently shown in the OSD, tracked by the server.
readonly property alias popups: server.trackedNotifications
readonly property int popupCount: popups.values.length
// Do-not-disturb: notifications are recorded but not displayed.
property bool dnd: false
// Maximum amount of simultaneously displayed popups.
property int maxPopups: 5
// History of received notifications (newest first), for future dashboards.
property var history: []
property int historyLimit: 100
NotificationServer {
id: server
keepOnReload: false
bodySupported: true
bodyMarkupSupported: true
actionsSupported: true
onNotification: notification => {
root.addHistory(notification);
if (root.dnd || notification.transient)
return;
notification.tracked = true;
const tracked = server.trackedNotifications.values;
if (tracked.length > root.maxPopups)
tracked[0].expire();
}
}
function clearHistory(): void {
root.history = [];
}
function addHistory(notification: Notification): void {
const entry = {
id: notification.id,
appName: notification.appName,
appIcon: notification.appIcon,
desktopEntry: notification.desktopEntry,
summary: notification.summary,
body: notification.body,
image: notification.image,
urgency: notification.urgency,
transient: notification.transient,
time: Date.now(),
actions: notification.actions.map(action => ({
identifier: action.identifier,
text: action.text
}))
};
let next = [entry, ...root.history];
if (next.length > root.historyLimit)
next = next.slice(0, root.historyLimit);
root.history = next;
}
}

View File

@@ -3,4 +3,6 @@ import qs.windows
Scope { Scope {
Bar {} Bar {}
Notification {}
} }

55
windows/Notification.qml Normal file
View File

@@ -0,0 +1,55 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.config
import qs.services
import "../modules/osd"
PanelWindow {
id: root
readonly property int cardWidth: 420
color: "transparent"
exclusionMode: ExclusionMode.Ignore
visible: Notifications.popupCount > 0
implicitWidth: root.cardWidth
implicitHeight: stack.implicitHeight
anchors {
top: true
right: true
}
margins {
top: 50 + Appearance.margin
right: 10 + Appearance.margin
}
mask: Region {
item: stack
}
ColumnLayout {
id: stack
anchors.top: parent.top
anchors.right: parent.right
anchors.left: parent.left
spacing: Appearance.spacing
Repeater {
model: [...Notifications.popups.values].reverse()
NotificationCard {
id: card
required property var modelData
notif: modelData
Layout.fillWidth: true
}
}
}
}