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:
2026-04-05 20:00:54 +02:00
parent 6e55544c42
commit c5f7162ebb
31 changed files with 3691 additions and 0 deletions

View 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
}
}
}
}
}
}
}
}

View 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
}
}