From 2382ee653737900030e5ece764adefbaaa749303 Mon Sep 17 00:00:00 2001 From: Tim Wundenberg Date: Wed, 15 Apr 2026 17:25:01 +0200 Subject: [PATCH] Colors and Notifications --- Colors.qml | 15 ++ bar/Bar.qml | 3 +- bar/CalendarPopup.qml | 87 ++++----- bar/DateTime.qml | 31 +++- bar/SystemStats.qml | 59 ++++--- bar/Workspaces.qml | 12 +- notifications/NotificationPopups.qml | 255 +++++++++++++++++++++++++++ qmldir | 1 + shell.qml | 5 +- 9 files changed, 372 insertions(+), 96 deletions(-) create mode 100644 Colors.qml create mode 100644 notifications/NotificationPopups.qml create mode 100644 qmldir diff --git a/Colors.qml b/Colors.qml new file mode 100644 index 0000000..0378f32 --- /dev/null +++ b/Colors.qml @@ -0,0 +1,15 @@ +pragma Singleton + +import QtQuick + +// Everforest Dark Hard — edit here to retheme the whole bar. +QtObject { + readonly property color bg: "#2d353b" // bar background, text on highlights + readonly property color bgDark: "#272e33" // popup background + readonly property color surface: "#343f44" // inactive workspace pill, hover bg + readonly property color fg: "#d3c6aa" // primary text and icons + readonly property color fgDim: "#7a8478" // separators, weekday labels, muted/offline + readonly property color accent: "#a7c080" // active workspace, today, nav arrows + readonly property color warning: "#e69875" // moderate load + readonly property color critical: "#e67e80" // high load, low battery, weekends +} diff --git a/bar/Bar.qml b/bar/Bar.qml index 748d2ee..64e0088 100644 --- a/bar/Bar.qml +++ b/bar/Bar.qml @@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound import Quickshell import QtQuick import QtQuick.Layouts +import ".." PanelWindow { id: root @@ -16,7 +17,7 @@ PanelWindow { implicitHeight: 32 exclusionMode: ExclusionMode.Auto - color: "#1e1e2e" + color: Colors.bg // Left side — anchored to left edge, never affected by right side width RowLayout { diff --git a/bar/CalendarPopup.qml b/bar/CalendarPopup.qml index b4d5e4e..0a5176e 100644 --- a/bar/CalendarPopup.qml +++ b/bar/CalendarPopup.qml @@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound import Quickshell import QtQuick import QtQuick.Layouts +import ".." // Calendar popup — anchored below its trigger item. // Instantiate inside DateTime.qml; set anchor.window + anchor.item from there. @@ -15,7 +16,7 @@ PopupWindow { visible: false color: "transparent" - implicitWidth: 252 // 7 columns × 36 px + implicitWidth: 276 // 7 columns × 36 px + 12 px margin on each side implicitHeight: 284 // ── State ──────────────────────────────────────────────────────────────── @@ -24,7 +25,7 @@ PopupWindow { // ── Public API ─────────────────────────────────────────────────────────── function open() { - // Reset animation state before the window becomes visible. + // Reset content state in case closeAnim ran previously. content.opacity = 0; content.scale = 0.95; closeAnim.stop(); @@ -114,7 +115,7 @@ PopupWindow { // ── Background ─────────────────────────────────────────────────────── Rectangle { anchors.fill: parent - color: "#181825" // Catppuccin Mantle + color: Colors.bgDark radius: 8 } @@ -127,63 +128,35 @@ PopupWindow { spacing: 8 // ── Month navigation ───────────────────────────────────────────── + component NavButton: Rectangle { + required property string label + required property var action + width: 24; height: 24; radius: 4 + color: hover.hovered ? Colors.surface : "transparent" + Text { + anchors.centerIn: parent + text: parent.label + color: Colors.accent + font.pixelSize: 16 + font.weight: Font.Bold + } + HoverHandler { id: hover } + TapHandler { onTapped: parent.action() } + } + RowLayout { width: parent.width - Rectangle { - width: 24 - height: 24 - radius: 4 - color: prevHover.hovered ? "#313244" : "transparent" - Text { - anchors.centerIn: parent - text: "‹" - color: "#89b4fa" - font.pixelSize: 16 - font.weight: Font.Bold - } - HoverHandler { - id: prevHover - } - TapHandler { - onTapped: root.prevMonth() - } - } - - Item { - Layout.fillWidth: true - } - + NavButton { label: "‹"; action: root.prevMonth } + Item { Layout.fillWidth: true } Text { text: root.monthNames[root.viewMonth] + " " + root.viewYear - color: "#cdd6f4" + color: Colors.fg font.pixelSize: 13 font.weight: Font.Medium } - - Item { - Layout.fillWidth: true - } - - Rectangle { - width: 24 - height: 24 - radius: 4 - color: nextHover.hovered ? "#313244" : "transparent" - Text { - anchors.centerIn: parent - text: "›" - color: "#89b4fa" - font.pixelSize: 16 - font.weight: Font.Bold - } - HoverHandler { - id: nextHover - } - TapHandler { - onTapped: root.nextMonth() - } - } + Item { Layout.fillWidth: true } + NavButton { label: "›"; action: root.nextMonth } } // ── Day-of-week header ──────────────────────────────────────────── @@ -197,7 +170,7 @@ PopupWindow { width: 36 horizontalAlignment: Text.AlignHCenter text: modelData - color: index >= 5 ? "#f38ba8" : "#6c7086" + color: index >= 5 ? Colors.critical : Colors.fgDim font.pixelSize: 11 } } @@ -243,7 +216,7 @@ PopupWindow { width: 28 height: 28 radius: 14 - color: cell.isToday ? "#89b4fa" : "transparent" + color: cell.isToday ? Colors.accent : "transparent" } Text { @@ -254,10 +227,10 @@ PopupWindow { font.weight: cell.isToday ? Font.Bold : Font.Normal color: { if (cell.isToday) - return "#1e1e2e"; + return Colors.bg; if (cell.isWeekend) - return "#f38ba8"; - return "#cdd6f4"; + return Colors.critical; + return Colors.fg; } } } diff --git a/bar/DateTime.qml b/bar/DateTime.qml index a664d66..ec30d90 100644 --- a/bar/DateTime.qml +++ b/bar/DateTime.qml @@ -1,8 +1,10 @@ pragma ComponentBehavior: Bound import Quickshell +import Quickshell.Wayland import QtQuick import QtQuick.Layouts +import ".." // Date + time widget. Click it to open/close the calendar popup. // barWindow must be set by the parent (Bar.qml) so the popup has a Wayland parent. @@ -20,24 +22,45 @@ RowLayout { anchor.window: root.barWindow anchor.item: root - anchor.edges: Edges.Bottom + anchor.edges: Edges.Bottom | Edges.Right + anchor.gravity: Edges.Bottom | Edges.Left + } + + // Full-screen transparent overlay: captures outside clicks and Escape to close calendar. + // WlrLayershell.keyboardFocus: Exclusive gives it real keyboard focus from the compositor. + PanelWindow { + visible: cal.visible + screen: root.barWindow.screen + color: "transparent" + exclusionMode: ExclusionMode.Ignore + anchors { top: true; bottom: true; left: true; right: true } + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + + TapHandler { + onTapped: cal.close() + } + + Item { + focus: true + Keys.onEscapePressed: cal.close() + } } Text { id: dateLabel - color: "#a6adc8" + color: Colors.fg font.pixelSize: 13 } Text { text: "|" - color: "#45475a" + color: Colors.fgDim font.pixelSize: 13 } Text { id: timeLabel - color: "#cdd6f4" + color: Colors.fg font.pixelSize: 13 font.weight: Font.Medium Behavior on color { diff --git a/bar/SystemStats.qml b/bar/SystemStats.qml index 7347c4a..98bd4ed 100644 --- a/bar/SystemStats.qml +++ b/bar/SystemStats.qml @@ -4,6 +4,7 @@ import Quickshell import Quickshell.Io import QtQuick import QtQuick.Layouts +import ".." // System stats strip: Volume · Network · CPU load · Memory · Temperature · Brightness · Battery // All stats refreshed every 5 s via a single bash process. @@ -20,6 +21,7 @@ RowLayout { property int cpuCores: 1 property real memUsedGB: 0 property real memTotalGB: 0 + readonly property real memRatio: memTotalGB > 0 ? memUsedGB / memTotalGB : 0 property int tempC: 0 property int brightPct: 100 property int batPct: -1 // -1 means no battery detected @@ -113,8 +115,9 @@ RowLayout { } // ── Volume ──────────────────────────────────────────────────────────────── - RowLayout { - spacing: 4 + Item { + implicitWidth: volRow.implicitWidth + implicitHeight: volRow.implicitHeight Process { id: pavucontrolProc; command: ["pavucontrol"] } Process { id: volUpProc; command: ["pactl", "set-sink-volume", "@DEFAULT_SINK@", "+5%"] } @@ -137,15 +140,21 @@ RowLayout { } } - Text { - text: root.volMuted ? "󰖁" : (root.volPct > 50 ? "󰕾" : root.volPct > 0 ? "󰖀" : "󰕿") - color: root.volMuted ? "#585b70" : "#cdd6f4" - font.pixelSize: 15 - } - Text { - text: root.volMuted ? "mute" : root.volPct + "%" - color: "#a6adc8" - font.pixelSize: 13 + RowLayout { + id: volRow + anchors.fill: parent + spacing: 4 + + Text { + text: root.volMuted ? "󰖁" : (root.volPct > 50 ? "󰕾" : root.volPct > 0 ? "󰖀" : "󰕿") + color: root.volMuted ? Colors.fgDim : Colors.fg + font.pixelSize: 15 + } + Text { + text: root.volMuted ? "mute" : root.volPct + "%" + color: Colors.fg + font.pixelSize: 13 + } } } @@ -154,13 +163,13 @@ RowLayout { spacing: 4 Text { text: root.netIp ? "󰱓" : "󰲛" - color: root.netIp ? "#cdd6f4" : "#585b70" + color: root.netIp ? Colors.fg : Colors.fgDim font.pixelSize: 15 } Text { visible: root.netIp !== "" text: root.netIp - color: "#a6adc8" + color: Colors.fg font.pixelSize: 13 } } @@ -168,11 +177,11 @@ RowLayout { // ── CPU load average ────────────────────────────────────────────────────── RowLayout { spacing: 4 - Text { text: "󰻠"; color: "#cdd6f4"; font.pixelSize: 15 } + Text { text: "󰻠"; color: Colors.fg; font.pixelSize: 15 } Text { text: root.loadAvg.toFixed(2) - color: root.loadAvg > root.cpuCores ? "#f38ba8" : - root.loadAvg > root.cpuCores * 0.7 ? "#fab387" : "#a6adc8" + color: root.loadAvg > root.cpuCores ? Colors.critical : + root.loadAvg > root.cpuCores * 0.7 ? Colors.warning : Colors.fg font.pixelSize: 13 } } @@ -180,11 +189,11 @@ RowLayout { // ── Memory ──────────────────────────────────────────────────────────────── RowLayout { spacing: 4 - Text { text: "󰍛"; color: "#cdd6f4"; font.pixelSize: 15 } + Text { text: "󰍛"; color: Colors.fg; font.pixelSize: 15 } Text { text: root.memUsedGB.toFixed(1) + "/" + root.memTotalGB.toFixed(1) + "G" - color: root.memTotalGB > 0 && (root.memUsedGB / root.memTotalGB) > 0.8 ? "#f38ba8" : - root.memTotalGB > 0 && (root.memUsedGB / root.memTotalGB) > 0.6 ? "#fab387" : "#a6adc8" + color: root.memRatio > 0.8 ? Colors.critical : + root.memRatio > 0.6 ? Colors.warning : Colors.fg font.pixelSize: 13 } } @@ -192,10 +201,10 @@ RowLayout { // ── Temperature ─────────────────────────────────────────────────────────── RowLayout { spacing: 4 - Text { text: "󰔐"; color: "#cdd6f4"; font.pixelSize: 15 } + Text { text: "󰔐"; color: Colors.fg; font.pixelSize: 15 } Text { text: root.tempC + "°" - color: root.tempC > 85 ? "#f38ba8" : root.tempC > 65 ? "#fab387" : "#a6adc8" + color: root.tempC > 85 ? Colors.critical : root.tempC > 65 ? Colors.warning : Colors.fg font.pixelSize: 13 } } @@ -203,10 +212,10 @@ RowLayout { // ── Brightness ──────────────────────────────────────────────────────────── RowLayout { spacing: 4 - Text { text: "󰃟"; color: "#cdd6f4"; font.pixelSize: 15 } + Text { text: "󰃟"; color: Colors.fg; font.pixelSize: 15 } Text { text: root.brightPct + "%" - color: "#a6adc8" + color: Colors.fg font.pixelSize: 13 } } @@ -221,12 +230,12 @@ RowLayout { root.batPct > 60 ? "󰁾" : root.batPct > 40 ? "󰁼" : root.batPct > 20 ? "󰁺" : "󰂃" - color: root.batPct < 20 && root.batStatus !== "Charging" ? "#f38ba8" : "#cdd6f4" + color: root.batPct < 20 && root.batStatus !== "Charging" ? Colors.critical : Colors.fg font.pixelSize: 15 } Text { text: root.batTime ? root.batPct + "% · " + root.batTime : root.batPct + "%" - color: root.batPct < 20 && root.batStatus !== "Charging" ? "#f38ba8" : "#a6adc8" + color: root.batPct < 20 && root.batStatus !== "Charging" ? Colors.critical : Colors.fg font.pixelSize: 13 } } diff --git a/bar/Workspaces.qml b/bar/Workspaces.qml index b082ad6..571da5d 100644 --- a/bar/Workspaces.qml +++ b/bar/Workspaces.qml @@ -4,6 +4,7 @@ import Quickshell import Quickshell.Hyprland import QtQuick import QtQuick.Layouts +import ".." // A row of clickable workspace buttons for one screen. // The button for the workspace currently active on *this* screen is highlighted. @@ -26,18 +27,13 @@ RowLayout { required property HyprlandWorkspace modelData readonly property bool isActive: root.monitor ? (root.monitor.activeWorkspace?.id === modelData.id) : modelData.focused + readonly property bool isUrgent: modelData.urgent implicitWidth: Math.max(28, label.implicitWidth + 16) implicitHeight: 22 radius: 4 - color: { - if (isActive) - return "#89b4fa"; - if (hoverArea.hovered) - return "#45475a"; - return "#313244"; - } + color: isActive ? Colors.accent : isUrgent ? Colors.warning : Colors.surface Behavior on color { ColorAnimation { @@ -49,7 +45,7 @@ RowLayout { id: label anchors.centerIn: parent text: btn.modelData.name - color: btn.isActive ? "#1e1e2e" : "#cdd6f4" // Base / Text + color: (btn.isActive || btn.isUrgent) ? Colors.bg : Colors.fg font.pixelSize: 13 font.weight: btn.isActive ? Font.Bold : Font.Normal diff --git a/notifications/NotificationPopups.qml b/notifications/NotificationPopups.qml new file mode 100644 index 0000000..ac89052 --- /dev/null +++ b/notifications/NotificationPopups.qml @@ -0,0 +1,255 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Services.Notifications +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts +import ".." + +// Notification toast overlay. +// Toasts slide in from the right, stack downward, and auto-dismiss. +// Urgency colouring: Low → fgDim stripe, Normal → accent stripe, Critical → critical border + stripe. + +PanelWindow { + id: root + + anchors { + top: true + right: true + } + exclusionMode: ExclusionMode.Ignore + color: "transparent" + + implicitWidth: 356 // 340 toast + 8 left + 8 right margin + implicitHeight: Math.max(1, (clearAllBtn.visible ? clearAllBtn.height + 6 : 0) + notifList.contentHeight + 16) + + // ── Notification server ────────────────────────────────────────────────── + NotificationServer { + id: server + + onNotification: function (notif) { + notifModel.insert(0, { + "nid": notif.id, + "app": notif.appName, + "icon": notif.appIcon, + "summary": notif.summary !== "" ? notif.summary : notif.appName, + "body": notif.body, + "urgency": notif.urgency + }); + } + } + + ListModel { + id: notifModel + } + + function removeById(nid) { + for (let i = 0; i < notifModel.count; i++) { + if (notifModel.get(i).nid === nid) { + notifModel.remove(i); + return; + } + } + } + + // ── Toast list ─────────────────────────────────────────────────────────── + // ── Clear all button (visible when 2+ notifications) ──────────────────── + Rectangle { + id: clearAllBtn + visible: notifModel.count >= 2 + anchors { top: parent.top; right: parent.right; topMargin: 8; rightMargin: 8 } + width: 340 + height: 26 + color: clearHover.hovered ? Colors.surface : Colors.bgDark + border.color: Colors.surface + border.width: 1 + radius: 6 + + Text { + anchors.centerIn: parent + text: "Clear all (" + notifModel.count + ")" + color: Colors.fgDim + font.pixelSize: 12 + } + + HoverHandler { id: clearHover } + TapHandler { onTapped: notifModel.clear() } + } + + ListView { + id: notifList + anchors { + top: clearAllBtn.visible ? clearAllBtn.bottom : parent.top + right: parent.right + topMargin: clearAllBtn.visible ? 6 : 8 + rightMargin: 8 + } + width: 340 + height: contentHeight + model: notifModel + spacing: 6 + interactive: false + + add: Transition { + NumberAnimation { + property: "x" + from: 356 + to: 0 + duration: 220 + easing.type: Easing.OutCubic + } + NumberAnimation { + property: "opacity" + from: 0 + to: 1 + duration: 180 + } + } + + remove: Transition { + NumberAnimation { + property: "opacity" + to: 0 + duration: 130 + easing.type: Easing.InCubic + } + NumberAnimation { + property: "x" + to: 356 + duration: 180 + easing.type: Easing.InCubic + } + } + + displaced: Transition { + NumberAnimation { + properties: "y" + duration: 220 + easing.type: Easing.OutCubic + } + NumberAnimation { + property: "opacity" + to: 1 + duration: 220 + } + } + + delegate: Item { + id: toast + required property int index + required property int nid + required property string app + required property string icon + required property string summary + required property string body + required property int urgency + + width: 340 + height: toastBg.height + + // ── Background card ────────────────────────────────────────────── + Rectangle { + id: toastBg + width: parent.width + height: cardContent.implicitHeight + 20 + color: Colors.bgDark + border.color: toast.urgency === 2 ? Colors.critical : Colors.surface + border.width: 1 + radius: 8 + + // Urgency stripe on left edge + Rectangle { + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + topMargin: 1 + bottomMargin: 1 + leftMargin: 1 + } + width: 3 + radius: 8 + color: toast.urgency === 0 ? Colors.fgDim : toast.urgency === 2 ? Colors.critical : Colors.accent + } + + ColumnLayout { + id: cardContent + anchors { + left: parent.left + right: parent.right + top: parent.top + leftMargin: 14 + rightMargin: 10 + topMargin: 10 + } + spacing: 4 + + // ── Header row: icon + summary + app name + close button ─ + RowLayout { + Layout.fillWidth: true + spacing: 6 + + // App icon (shown when available) + IconImage { + visible: toast.icon !== "" + source: toast.icon + width: 16 + height: 16 + mipmap: true + } + + Text { + text: toast.summary + color: toast.urgency === 2 ? Colors.critical : toast.urgency === 0 ? Colors.fgDim : Colors.accent + font.pixelSize: 13 + font.weight: Font.Medium + Layout.fillWidth: true + elide: Text.ElideRight + } + + Text { + text: toast.app + color: Colors.fgDim + font.pixelSize: 11 + elide: Text.ElideRight + Layout.maximumWidth: 90 + } + + Rectangle { + width: 18 + height: 18 + radius: 4 + color: closeHover.hovered ? Colors.surface : "transparent" + HoverHandler { + id: closeHover + } + TapHandler { + onTapped: root.removeById(toast.nid) + } + Text { + anchors.centerIn: parent + text: "✕" + color: Colors.fgDim + font.pixelSize: 11 + } + } + } + + // ── Body text ──────────────────────────────────────────── + Text { + visible: toast.body !== "" + text: toast.body + color: Colors.fg + font.pixelSize: 12 + Layout.fillWidth: true + wrapMode: Text.WordWrap + maximumLineCount: 3 + elide: Text.ElideRight + bottomPadding: 2 + } + } + } + } + } +} diff --git a/qmldir b/qmldir new file mode 100644 index 0000000..b7cddb9 --- /dev/null +++ b/qmldir @@ -0,0 +1 @@ +singleton Colors 1.0 Colors.qml diff --git a/shell.qml b/shell.qml index 914ea15..3bd839b 100644 --- a/shell.qml +++ b/shell.qml @@ -4,8 +4,9 @@ pragma ComponentBehavior: Bound import Quickshell import QtQuick import "bar" +import "notifications" -// Root scope — creates one Bar per connected screen. +// Root scope — creates one Bar per connected screen plus a single notification overlay. Scope { Variants { model: Quickshell.screens @@ -15,4 +16,6 @@ Scope { screen: modelData } } + + NotificationPopups {} }