quickshell: add initial bar config with per-monitor workspaces
- Vertical bar on DP-2 with rounded-square pills throughout - Per-monitor workspace groups sorted by screen x position, with Nerd Font icons for named workspaces and apex-neon red active indicator - Bar layout: datetime+weather top, workspaces centered, gamemode+media+notif+system bottom - Popouts anchor to triggering icon (top-right for datetime/weather, bottom-right for media/notif/system) - Lock command switched from hyprlock to swaylock - Hyprland blur/ignore_alpha layerrules for quickshell namespace Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
159
dot_config/quickshell/notifications/NotificationDaemon.qml
Normal file
159
dot_config/quickshell/notifications/NotificationDaemon.qml
Normal file
@@ -0,0 +1,159 @@
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import Quickshell.Wayland
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../shared" as Shared
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
// Expose for NC popout
|
||||
property alias trackedNotifications: server.trackedNotifications
|
||||
property bool dnd: false
|
||||
|
||||
// Toast IDs currently showing as popups (capped to avoid overflow)
|
||||
readonly property int maxToasts: 4
|
||||
property var toastIds: []
|
||||
property var timestamps: ({}) // notification id → arrival epoch ms
|
||||
|
||||
NotificationServer {
|
||||
id: server
|
||||
keepOnReload: true
|
||||
bodySupported: true
|
||||
bodyMarkupSupported: false
|
||||
actionsSupported: true
|
||||
imageSupported: true
|
||||
persistenceSupported: true
|
||||
|
||||
onNotification: notification => {
|
||||
notification.tracked = true;
|
||||
root.timestamps[notification.id] = Date.now();
|
||||
root.timestampsChanged();
|
||||
|
||||
// Suppress toasts in DnD mode (still tracked for history)
|
||||
if (root.dnd) return;
|
||||
|
||||
// Add to toast list, evict oldest if at capacity
|
||||
let ids = root.toastIds.slice();
|
||||
if (ids.length >= root.maxToasts) {
|
||||
let evicted = ids.shift();
|
||||
delete toastTimer.pending[evicted];
|
||||
}
|
||||
ids.push(notification.id);
|
||||
root.toastIds = ids;
|
||||
|
||||
// Schedule toast removal (not notification removal)
|
||||
let timeout = notification.expireTimeout > 0 ? notification.expireTimeout * 1000 : 5000;
|
||||
if (notification.urgency !== NotificationUrgency.Critical) {
|
||||
toastTimer.createTimer(notification.id, timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Toast timer manager
|
||||
QtObject {
|
||||
id: toastTimer
|
||||
property var pending: ({})
|
||||
|
||||
function createTimer(id, timeout) {
|
||||
pending[id] = Date.now() + timeout;
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 500
|
||||
running: root.toastIds.length > 0
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
let now = Date.now();
|
||||
let changed = false;
|
||||
let ids = root.toastIds.slice();
|
||||
|
||||
for (let id in toastTimer.pending) {
|
||||
if (now >= toastTimer.pending[id]) {
|
||||
let idx = ids.indexOf(parseInt(id));
|
||||
if (idx >= 0) { ids.splice(idx, 1); changed = true; }
|
||||
delete toastTimer.pending[id];
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) root.toastIds = ids;
|
||||
}
|
||||
}
|
||||
|
||||
// Prune stale timestamps to prevent unbounded growth
|
||||
Timer {
|
||||
interval: 60000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
let tracked = server.trackedNotifications;
|
||||
let live = new Set();
|
||||
for (let i = 0; i < tracked.values.length; i++)
|
||||
live.add(tracked.values[i].id);
|
||||
let ts = root.timestamps;
|
||||
let pruned = false;
|
||||
for (let id in ts) {
|
||||
if (!live.has(parseInt(id))) { delete ts[id]; pruned = true; }
|
||||
}
|
||||
if (pruned) root.timestampsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
// Toast popup window — shows only active toasts
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
delegate: Component {
|
||||
PanelWindow {
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
WlrLayershell.namespace: "quickshell:notifications"
|
||||
surfaceFormat { opaque: false }
|
||||
|
||||
visible: modelData.name === Shared.Config.monitor && root.toastIds.length > 0
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
right: true
|
||||
}
|
||||
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
implicitWidth: 380
|
||||
implicitHeight: toastColumn.implicitHeight + 20
|
||||
color: "transparent"
|
||||
|
||||
margins {
|
||||
right: Shared.Theme.barWidth + 12
|
||||
top: 8
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: toastColumn
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
width: 360
|
||||
spacing: 8
|
||||
|
||||
Repeater {
|
||||
model: server.trackedNotifications
|
||||
|
||||
Loader {
|
||||
required property var modelData
|
||||
active: root.toastIds.indexOf(modelData.id) >= 0
|
||||
visible: active
|
||||
Layout.fillWidth: true
|
||||
|
||||
sourceComponent: NotificationPopup {
|
||||
notification: modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
163
dot_config/quickshell/notifications/NotificationPopup.qml
Normal file
163
dot_config/quickshell/notifications/NotificationPopup.qml
Normal file
@@ -0,0 +1,163 @@
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../shared" as Shared
|
||||
|
||||
// Single notification card
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
required property var notification
|
||||
|
||||
implicitWidth: 360
|
||||
implicitHeight: content.implicitHeight + 20
|
||||
radius: Shared.Theme.radiusNormal
|
||||
color: Shared.Theme.popoutBackground
|
||||
border.width: 1
|
||||
border.color: notification.urgency === NotificationUrgency.Critical ? Qt.alpha(Shared.Theme.danger, Shared.Theme.opacityMedium) : Shared.Theme.borderSubtle
|
||||
|
||||
// Entrance animation
|
||||
opacity: 0
|
||||
scale: 0.95
|
||||
Component.onCompleted: { opacity = 1; scale = 1; }
|
||||
|
||||
Behavior on opacity { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
|
||||
Behavior on scale { NumberAnimation { duration: 180; easing.type: Easing.OutCubic } }
|
||||
|
||||
// Dismiss on click
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: root.notification.dismiss()
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 6
|
||||
|
||||
// Header: icon + app name + close
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
IconImage {
|
||||
source: root.notification.appIcon
|
||||
implicitSize: 18
|
||||
visible: root.notification.appIcon !== ""
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.notification.appName || "Notification"
|
||||
color: Shared.Theme.overlay0
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "\u{f0156}"
|
||||
color: closeMouse.containsMouse ? Shared.Theme.danger : Shared.Theme.overlay0
|
||||
font.pixelSize: 14
|
||||
font.family: Shared.Theme.iconFont
|
||||
MouseArea {
|
||||
id: closeMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: root.notification.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Summary (title)
|
||||
Text {
|
||||
visible: text !== ""
|
||||
text: root.notification.summary
|
||||
color: Shared.Theme.text
|
||||
font.pixelSize: Shared.Theme.fontSize
|
||||
font.family: Shared.Theme.fontFamily
|
||||
font.bold: true
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 2
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
// Body
|
||||
Text {
|
||||
visible: text !== ""
|
||||
text: root.notification.body
|
||||
textFormat: Text.PlainText
|
||||
color: Shared.Theme.subtext0
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 4
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
// Image
|
||||
Image {
|
||||
visible: root.notification.image !== ""
|
||||
source: root.notification.image
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 120
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
Layout.topMargin: 2
|
||||
}
|
||||
|
||||
// Actions
|
||||
RowLayout {
|
||||
visible: root.notification.actions.length > 0
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 2
|
||||
spacing: 6
|
||||
|
||||
Repeater {
|
||||
model: root.notification.actions
|
||||
|
||||
Rectangle {
|
||||
required property var modelData
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 28
|
||||
radius: 6
|
||||
color: actMouse.containsMouse ? Shared.Theme.surface1 : Shared.Theme.surface0
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 100 } }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData.text
|
||||
color: Shared.Theme.text
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: actMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: modelData.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Urgency accent bar on left edge
|
||||
Rectangle {
|
||||
visible: root.notification.urgency === NotificationUrgency.Critical
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.leftMargin: 1
|
||||
anchors.topMargin: root.radius
|
||||
anchors.bottomMargin: root.radius
|
||||
width: 3
|
||||
color: Shared.Theme.danger
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user