aboutsummaryrefslogtreecommitdiffstats
path: root/desktop
diff options
context:
space:
mode:
Diffstat (limited to 'desktop')
-rw-r--r--desktop/Drawer.qml58
-rw-r--r--desktop/NotificationHistory.qml70
-rw-r--r--desktop/NotificationList.qml90
-rw-r--r--desktop/NotificationRow.qml126
l---------desktop/Notify.qml1
-rw-r--r--desktop/modules/status/SnoozeRow.qml114
-rw-r--r--desktop/modules/status/StatusPage.qml10
7 files changed, 460 insertions, 9 deletions
diff --git a/desktop/Drawer.qml b/desktop/Drawer.qml
index 4ed37d3..c02a4ba 100644
--- a/desktop/Drawer.qml
+++ b/desktop/Drawer.qml
@@ -10,6 +10,7 @@
// GNU General Public License for more details.
import Quickshell
+import Quickshell.Io
import Quickshell.Wayland
import QtQuick
@@ -29,6 +30,25 @@ Scope {
// Which module's page is showing. Empty means the grid.
property string page: ""
+ // Set by NotificationList's History button and read by the history view
+ // loader below; reset when the drawer closes.
+ property bool history: false
+
+ // The balloons read this to stand down while the drawer holds the space.
+ FileView {
+ id: drawerFlag
+ path: (Quickshell.env("XDG_RUNTIME_DIR") || "/tmp") + "/notifyd/drawer"
+ atomicWrites: true
+ printErrors: false
+ }
+
+ onOpenChanged: drawerFlag.setText(root.open ? "1\n" : "0\n")
+
+ // onOpenChanged does not fire for the initial value, so a restart while
+ // the drawer was open would leave the flag stale and suppress every
+ // balloon. Publish the starting state explicitly.
+ Component.onCompleted: drawerFlag.setText(root.open ? "1\n" : "0\n")
+
readonly property var screenObj:
Quickshell.screens.find(s => s.name === root.monitor) ?? Quickshell.screens[0]
@@ -41,6 +61,9 @@ Scope {
}
function show(name) {
+ // Deep-linking to a page must leave the history view, or both loaders
+ // stay active and the later-declared history loader paints on top.
+ root.history = false;
root.page = root.modules.some(m => m.name === name) ? name : "";
root.open = true;
}
@@ -50,6 +73,7 @@ Scope {
// Reset to the grid: a panel that reopens somewhere unexpected is
// worse than one extra click.
root.page = "";
+ root.history = false;
}
function toggle(name) {
@@ -109,7 +133,8 @@ Scope {
// to it.
focus: true
Keys.onEscapePressed: {
- if (root.page) root.page = "";
+ if (root.history) root.history = false;
+ else if (root.page) root.page = "";
else root.close();
}
@@ -121,18 +146,17 @@ Scope {
Item {
anchors.fill: parent
anchors.margins: 16
- visible: root.page === ""
-
- // Reserved for the notification engine. An empty Item that
- // claims the space rather than a placeholder graphic: the
- // grid has to sit where it will sit once notifications
- // arrive, or the layout is tuned against a position that
- // does not survive.
- Item {
+ visible: root.page === "" && !root.history
+
+ // The notification engine's space. The live queue lists
+ // here while the drawer is open; the balloons stand down
+ // because the drawer flag above is set.
+ NotificationList {
id: notifications
anchors { top: parent.top; left: parent.left; right: parent.right }
anchors.bottom: grid.top
anchors.bottomMargin: 16
+ onHistory: root.history = true
}
// Fixed, never scrolled. Three columns at 600px with a
@@ -196,6 +220,22 @@ Scope {
}
Behavior on opacity { NumberAnimation { duration: 160 } }
}
+
+ // --- history view ---
+
+ Loader {
+ id: historyLoader
+ anchors.fill: parent
+ anchors.margins: 16
+ active: root.history
+ sourceComponent: Component {
+ Page {
+ title: "History"
+ NotificationHistory { width: parent.width }
+ }
+ }
+ onLoaded: if (item && item.back) item.back.connect(() => root.history = false)
+ }
}
}
}
diff --git a/desktop/NotificationHistory.qml b/desktop/NotificationHistory.qml
new file mode 100644
index 0000000..8499192
--- /dev/null
+++ b/desktop/NotificationHistory.qml
@@ -0,0 +1,70 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+import QtQuick
+
+// The history ring: the last 20 dismissed or evicted notifications, newest
+// first, with a clear all. Rows are inert; only clear-all removes them, since
+// the daemon has no per-id history deletion.
+Column {
+ id: page
+
+ spacing: 8
+
+ Item {
+ width: parent.width
+ height: 28
+
+ Text {
+ anchors { left: parent.left; verticalCenter: parent.verticalCenter }
+ text: Notify.history.length + " in history"
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
+ color: Theme.subtext
+ }
+
+ Text {
+ id: clearBtn
+ anchors { right: parent.right; verticalCenter: parent.verticalCenter }
+ visible: Notify.history.length > 0
+ text: "Clear all"
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
+ color: clearArea.containsMouse ? Theme.red : Theme.subtext
+
+ MouseArea {
+ id: clearArea
+ anchors.fill: parent
+ anchors.margins: -6
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: Notify.clearHistory()
+ }
+ }
+ }
+
+ Repeater {
+ model: Notify.history
+
+ NotificationRow {
+ required property var modelData
+ width: page.width
+ notification: modelData
+ live: false
+ }
+ }
+
+ Text {
+ width: parent.width
+ visible: Notify.history.length === 0
+ text: "Nothing in history"
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
+ color: Theme.overlay
+ }
+}
diff --git a/desktop/NotificationList.qml b/desktop/NotificationList.qml
new file mode 100644
index 0000000..9fe4328
--- /dev/null
+++ b/desktop/NotificationList.qml
@@ -0,0 +1,90 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+import QtQuick
+
+// The drawer's reserved notification space: a header with a History button,
+// then the live queue, scrollable because the queue can hold 20 and the grid
+// below is fixed. The grid owns its own position, so this space only fills
+// what the grid leaves.
+Item {
+ id: list
+
+ signal history
+
+ Column {
+ anchors.fill: parent
+ spacing: 6
+
+ Item {
+ width: parent.width
+ height: 28
+
+ Text {
+ anchors { left: parent.left; verticalCenter: parent.verticalCenter }
+ text: "Notifications"
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4; bold: true }
+ color: Theme.subtext
+ }
+
+ Text {
+ id: historyBtn
+ anchors { right: parent.right; verticalCenter: parent.verticalCenter }
+ text: "History"
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
+ color: historyArea.containsMouse ? Theme.accent : Theme.subtext
+
+ MouseArea {
+ id: historyArea
+ anchors.fill: parent
+ anchors.margins: -6
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: list.history()
+ }
+ }
+ }
+
+ Flickable {
+ id: flick
+ width: parent.width
+ height: parent.height - 34
+ clip: true
+ contentWidth: width
+ contentHeight: col.implicitHeight
+ boundsBehavior: Flickable.StopAtBounds
+
+ Column {
+ id: col
+ width: flick.width
+ spacing: 8
+
+ Repeater {
+ model: Notify.queue
+ NotificationRow {
+ required property var modelData
+ width: col.width
+ notification: modelData
+ live: true
+ }
+ }
+
+ Text {
+ width: col.width
+ visible: Notify.queue.length === 0
+ text: "No notifications"
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
+ color: Theme.overlay
+ }
+ }
+ }
+ }
+}
diff --git a/desktop/NotificationRow.qml b/desktop/NotificationRow.qml
new file mode 100644
index 0000000..923cc9e
--- /dev/null
+++ b/desktop/NotificationRow.qml
@@ -0,0 +1,126 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+import QtQuick
+
+// One notification in the drawer: app, summary and body. Live rows close with
+// the X and click through to their actions; a history row is inert.
+Rectangle {
+ id: row
+
+ radius: 8
+ color: Qt.alpha(Theme.surface, 0.6)
+ border.width: 1
+ border.color: Qt.alpha(Theme.text, 0.08)
+
+ required property var notification
+ // A history row has no live client: no X, and a click does nothing.
+ property bool live: true
+
+ readonly property bool inert: row.notification.expires !== 0 && Date.now() >= row.notification.expires
+
+ implicitHeight: texts.implicitHeight + 24
+
+ // "Sep 15 14:32" from the notification's created epoch-ms, local time. No
+ // year: the history ring is only 20 entries, so everything here is recent.
+ function stamp(ms) {
+ const d = new Date(ms);
+ const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
+ function pad(n) { return (n < 10 ? "0" : "") + n; }
+ return months[d.getMonth()] + " " + d.getDate() + " "
+ + pad(d.getHours()) + ":" + pad(d.getMinutes());
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ cursorShape: Qt.PointingHandCursor
+ onClicked: mouse => {
+ if (mouse.button === Qt.RightButton) {
+ Notify.closeAll();
+ return;
+ }
+ if (!row.live) return;
+ const acts = row.notification.actions || [];
+ if (acts.length > 0 && !row.inert) Notify.actions(row.notification.id, acts);
+ else Notify.close(row.notification.id);
+ }
+ }
+
+ Column {
+ id: texts
+ anchors {
+ left: parent.left
+ leftMargin: 12
+ right: closeBtn.visible ? closeBtn.left : parent.right
+ rightMargin: 10
+ verticalCenter: parent.verticalCenter
+ }
+ spacing: 2
+
+ Row {
+ id: head
+ width: parent.width
+ spacing: 8
+
+ Text {
+ width: head.width - (stamp.visible ? stamp.implicitWidth + head.spacing : 0)
+ text: (row.notification.app || "") + (row.notification.summary ? " " + row.notification.summary : "")
+ elide: Text.ElideRight
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize + 2 }
+ color: Theme.text
+ }
+
+ Text {
+ id: stamp
+ visible: row.notification.created > 0
+ text: visible ? row.stamp(row.notification.created) : ""
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
+ color: Theme.overlay
+ }
+ }
+
+ Text {
+ width: parent.width
+ visible: text !== ""
+ text: Notify.sanitize(row.notification.body)
+ textFormat: Text.RichText
+ wrapMode: Text.WordWrap
+ maximumLineCount: 2
+ elide: Text.ElideRight
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize }
+ color: Theme.subtext
+ }
+ }
+
+ Text {
+ id: closeBtn
+ visible: row.live
+ anchors { right: parent.right; rightMargin: 8; verticalCenter: parent.verticalCenter }
+ width: 20
+ height: 20
+ text: "\uf00d"
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ font { family: Theme.iconFamily; pixelSize: 13 }
+ color: closeArea.containsMouse ? Theme.red : Theme.subtext
+
+ MouseArea {
+ id: closeArea
+ anchors.fill: parent
+ anchors.margins: -6
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: Notify.close(row.notification.id)
+ }
+ }
+}
diff --git a/desktop/Notify.qml b/desktop/Notify.qml
new file mode 120000
index 0000000..635f31d
--- /dev/null
+++ b/desktop/Notify.qml
@@ -0,0 +1 @@
+../shared/Notify.qml \ No newline at end of file
diff --git a/desktop/modules/status/SnoozeRow.qml b/desktop/modules/status/SnoozeRow.qml
new file mode 100644
index 0000000..046d7cb
--- /dev/null
+++ b/desktop/modules/status/SnoozeRow.qml
@@ -0,0 +1,114 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+import Quickshell
+import Quickshell.Io
+import QtQuick
+import QtQuick.Controls
+import "../.."
+
+// A switch plus a minutes field. On snoozes for the typed minutes through
+// notify-snooze.sh; off clears. The last used value is persisted by the
+// script, so the field prefills from it.
+Item {
+ id: row
+
+ // A tick so the switch reflects the snooze ending on its own.
+ property double now: Date.now()
+ Timer {
+ interval: 1000
+ running: true
+ repeat: true
+ onTriggered: row.now = Date.now()
+ }
+
+ readonly property bool active: Notify.snoozeUntil > row.now
+
+ implicitHeight: Math.max(texts.implicitHeight, sw.implicitHeight) + 16
+
+ function apply(on) {
+ if (on) {
+ const n = parseInt(minutes.text, 10);
+ snooze.command = ["notify-snooze.sh", String(isNaN(n) || n < 1 ? 30 : n)];
+ } else {
+ snooze.command = ["notify-snooze.sh", "off"];
+ }
+ snooze.running = false;
+ snooze.running = true;
+ }
+
+ Process { id: snooze }
+
+ FileView {
+ id: lastUsed
+ path: `${Quickshell.env("HOME")}/.local/state/notify-snooze.minutes`
+ printErrors: false
+ onLoaded: minutes.text = text().trim()
+ onLoadFailed: minutes.text = "30"
+ }
+
+ Column {
+ id: texts
+ anchors {
+ left: parent.left
+ right: controls.left
+ rightMargin: 12
+ verticalCenter: parent.verticalCenter
+ }
+ spacing: 2
+
+ Text {
+ text: "Snooze"
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true }
+ color: Theme.text
+ }
+
+ Text {
+ width: parent.width
+ wrapMode: Text.WordWrap
+ text: row.active ? "All notification balloons are held until snooze ends."
+ : "Hold every notification balloon for a number of minutes."
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
+ color: Theme.subtext
+ }
+ }
+
+ Row {
+ id: controls
+ anchors { right: parent.right; verticalCenter: parent.verticalCenter }
+ spacing: 8
+
+ TextField {
+ id: minutes
+ width: 48
+ height: 28
+ text: "30"
+ horizontalAlignment: TextInput.AlignHCenter
+ color: Theme.text
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
+ background: Rectangle {
+ radius: 6
+ color: Qt.alpha(Theme.surface, 0.9)
+ border.width: 1
+ border.color: Qt.alpha(Theme.text, 0.15)
+ }
+ validator: IntValidator { bottom: 1; top: 1440 }
+ }
+
+ Switch {
+ id: sw
+ checked: row.active
+ onToggled: row.apply(!row.active)
+ }
+ }
+
+ onNowChanged: sw.checked = row.active
+}
diff --git a/desktop/modules/status/StatusPage.qml b/desktop/modules/status/StatusPage.qml
index 850cc73..2fbdc9b 100644
--- a/desktop/modules/status/StatusPage.qml
+++ b/desktop/modules/status/StatusPage.qml
@@ -40,4 +40,14 @@ Column {
description: "Do not disturb, no screen lock, breaktimer paused."
value: Status.presentation
}
+
+ Rectangle {
+ width: page.width
+ height: 1
+ color: Qt.alpha(Theme.text, 0.08)
+ }
+
+ SnoozeRow {
+ width: page.width
+ }
}