Compare commits

..

No commits in common. "d561d6f5c3b64386d7427087ba065cfc894d0e6c" and "472b4e62ab883bbc5c1f5f6bce548741b8554063" have entirely different histories.

9 changed files with 127 additions and 125 deletions

View file

@ -131,15 +131,7 @@ programs.nova-shell.modules = {
``` ```
Each module is an object with `enable` (default `true`) and optional extra Each module is an object with `enable` (default `true`) and optional extra
settings. For a full list of all options with types and defaults, build the docs: settings. Full list: `workspaces`, `tray`, `windowTitle`, `clock`,
```bash
nix build .#docs
# markdown: result/options.md
# html: result/index.html (open in browser)
```
Full list: `workspaces`, `tray`, `windowTitle`, `clock`,
`notifications`, `mpris`, `volume`, `bluetooth`, `backlight`, `network`, `notifications`, `mpris`, `volume`, `bluetooth`, `backlight`, `network`,
`powerProfile`, `idleInhibitor`, `weather`, `temperature`, `gpu`, `cpu`, `memory`, `powerProfile`, `idleInhibitor`, `weather`, `temperature`, `gpu`, `cpu`, `memory`,
`disk`, `battery`, `power`, `backgroundOverlay`, `overviewBackdrop`, `lock`. `disk`, `battery`, `power`, `backgroundOverlay`, `overviewBackdrop`, `lock`.

View file

@ -126,11 +126,6 @@ in
default = true; default = true;
description = "Show weather summary on the lock screen."; description = "Show weather summary on the lock screen.";
}; };
threatEffect = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Show red vignette and chromatic aberration on failed password attempts.";
};
}; };
notifications = moduleOpt "notifications" { notifications = moduleOpt "notifications" {
timeout = lib.mkOption { timeout = lib.mkOption {

View file

@ -10,7 +10,6 @@ HexWaveBackground 1.0 HexWaveBackground.qml
MemoryApplet 1.0 MemoryApplet.qml MemoryApplet 1.0 MemoryApplet.qml
MprisApplet 1.0 MprisApplet.qml MprisApplet 1.0 MprisApplet.qml
NetworkApplet 1.0 NetworkApplet.qml NetworkApplet 1.0 NetworkApplet.qml
NotifApplet 1.0 NotifApplet.qml
TemperatureApplet 1.0 TemperatureApplet.qml TemperatureApplet 1.0 TemperatureApplet.qml
VolumeApplet 1.0 VolumeApplet.qml VolumeApplet 1.0 VolumeApplet.qml
WeatherApplet 1.0 WeatherApplet.qml WeatherApplet 1.0 WeatherApplet.qml

View file

@ -16,8 +16,7 @@ WlSessionLockSurface {
property real _unlockFade: 1 property real _unlockFade: 1
// Threat level: 0.0-1.0 based on fail count (5 fails = max) // Threat level: 0.0-1.0 based on fail count (5 fails = max)
readonly property bool _threatEnabled: S.Modules.lock.threatEffect ?? true readonly property real _threat: Math.min(1.0, root.auth.failCount / 5)
readonly property real _threat: _threatEnabled ? Math.min(1.0, root.auth.failCount / 5) : 0
property real _heartbeat: 0 property real _heartbeat: 0
// All visual content wrapped for threat shader // All visual content wrapped for threat shader
@ -322,7 +321,7 @@ WlSessionLockSurface {
SequentialAnimation { SequentialAnimation {
id: _heartbeatAnim id: _heartbeatAnim
loops: Animation.Infinite loops: Animation.Infinite
running: root._threatEnabled && root.auth.failCount >= 3 && root.lock.secure running: root.auth.failCount >= 3 && root.lock.secure
// Systole (sharp spike) // Systole (sharp spike)
NumberAnimation { NumberAnimation {

View file

@ -114,6 +114,7 @@ PanelWindow {
visible: S.Modules.clock.enable visible: S.Modules.clock.enable
} }
M.NotificationsModule { M.NotificationsModule {
bar: bar
visible: S.Modules.notifications.enable visible: S.Modules.notifications.enable
} }
} }

View file

@ -1,26 +1,81 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import "." as M
import "../services" as S import "../services" as S
import "../modules" as M
Column { M.HoverPanel {
id: root id: menuWindow
required property color accentColor popupMode: true
required property int contentWidth contentWidth: 350
function cascadeDismiss() { // Header: title + clear all + DND toggle
_cascadeDismiss(); Item {
width: menuWindow.contentWidth
height: 32
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: "Notifications"
color: S.Theme.base05
font.pixelSize: S.Theme.fontSize + 1
font.family: S.Theme.fontFamily
font.bold: true
}
Row {
anchors.right: parent.right
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
spacing: 8
// DND toggle
Text {
text: S.NotifService.dnd ? "\uDB82\uDE93" : "\uDB80\uDC9C"
color: S.NotifService.dnd ? S.Theme.base09 : S.Theme.base04
font.pixelSize: S.Theme.fontSize
font.family: S.Theme.iconFontFamily
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: S.NotifService.toggleDnd()
}
}
// Clear all
Text {
text: "\uF1F8"
color: clearArea.containsMouse ? S.Theme.base08 : S.Theme.base04
font.pixelSize: S.Theme.fontSize
font.family: S.Theme.iconFontFamily
anchors.verticalCenter: parent.verticalCenter
visible: S.NotifService.count > 0
MouseArea {
id: clearArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: menuWindow._cascadeDismiss()
}
}
}
} }
// Cascade dismiss logic
property var _pendingDismissIds: [] property var _pendingDismissIds: []
// Collapsed groups set reassign to trigger reactivity
property var _collapsedGroups: ({}) property var _collapsedGroups: ({})
property real _savedScrollY: 0 property real _savedScrollY: 0
property bool _restoringScroll: false property bool _restoringScroll: false
function _toggleCollapse(appName) { function _toggleCollapse(appName) {
_savedScrollY = _notifList.contentY; _savedScrollY = notifList.contentY;
_restoringScroll = true; _restoringScroll = true;
const next = Object.assign({}, _collapsedGroups); const next = Object.assign({}, _collapsedGroups);
if (next[appName]) if (next[appName])
@ -30,6 +85,7 @@ Column {
_collapsedGroups = next; _collapsedGroups = next;
} }
// Group notifications by appName, sorted by max urgency desc then most recent time desc
readonly property var _groups: { readonly property var _groups: {
const map = {}; const map = {};
for (const n of S.NotifService.list) { for (const n of S.NotifService.list) {
@ -55,6 +111,7 @@ Column {
}); });
} }
// Flat model: group header followed by its notifications (omitted when collapsed)
readonly property var _flatModel: { readonly property var _flatModel: {
const arr = []; const arr = [];
for (const g of _groups) { for (const g of _groups) {
@ -78,6 +135,7 @@ Column {
return arr; return arr;
} }
// Collect visible (non-dismissing) notif delegates, optionally filtered by appName
function _getVisibleNotifDelegates(appName) { function _getVisibleNotifDelegates(appName) {
const result = []; const result = [];
for (let i = 0; i < _flatModel.length; i++) { for (let i = 0; i < _flatModel.length; i++) {
@ -86,7 +144,7 @@ Column {
continue; continue;
if (appName !== undefined && item.data.appName !== appName) if (appName !== undefined && item.data.appName !== appName)
continue; continue;
const d = _notifList.itemAtIndex(i); const d = notifList.itemAtIndex(i);
if (d && d._type === "notif" && d._notif?.state !== "dismissing") if (d && d._type === "notif" && d._notif?.state !== "dismissing")
result.push(d); result.push(d);
} }
@ -100,7 +158,7 @@ Column {
return; return;
} }
for (let i = 0; i < visibles.length; i++) { for (let i = 0; i < visibles.length; i++) {
_cascadeTimer.createObject(root, { _cascadeTimer.createObject(menuWindow, {
_target: visibles[i], _target: visibles[i],
_delay: i * 60, _delay: i * 60,
_isLast: i === visibles.length - 1 _isLast: i === visibles.length - 1
@ -140,7 +198,7 @@ Column {
if (_target && _target.dismissVisualOnly) if (_target && _target.dismissVisualOnly)
_target.dismissVisualOnly(); _target.dismissVisualOnly();
if (_isLast) if (_isLast)
root._bulkTimer.createObject(root, {}); _bulkTimer.createObject(menuWindow, {});
destroy(); destroy();
} }
} }
@ -148,28 +206,36 @@ Column {
property Component _bulkTimer: Component { property Component _bulkTimer: Component {
Timer { Timer {
interval: 400 interval: 400 // swipe (200) + collapse (150) + margin
running: true running: true
onTriggered: { onTriggered: {
root._finishCascade(); menuWindow._finishCascade();
destroy(); destroy();
} }
} }
} }
// Notification list // Separator
Rectangle {
width: menuWindow.contentWidth - 16
height: 1
anchors.horizontalCenter: parent.horizontalCenter
color: S.Theme.base03
}
// Notification list (scrollable)
ListView { ListView {
id: _notifList id: notifList
width: root.contentWidth width: menuWindow.contentWidth
height: Math.min(contentHeight, 60 * (S.Modules.notifications.maxVisible || 10)) height: Math.min(contentHeight, 60 * (S.Modules.notifications.maxVisible || 10))
clip: true clip: true
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
model: root._flatModel model: menuWindow._flatModel
onContentHeightChanged: { onContentHeightChanged: {
if (root._restoringScroll) { if (menuWindow._restoringScroll) {
contentY = Math.min(root._savedScrollY, Math.max(0, contentHeight - height)); contentY = Math.min(menuWindow._savedScrollY, Math.max(0, contentHeight - height));
root._restoringScroll = false; menuWindow._restoringScroll = false;
} }
} }
@ -181,7 +247,7 @@ Column {
readonly property string _type: modelData.type readonly property string _type: modelData.type
readonly property var _notif: _type === "notif" ? modelData.data : null readonly property var _notif: _type === "notif" ? modelData.data : null
width: root.contentWidth width: menuWindow.contentWidth
height: _displayTargetHeight * _heightScale height: _displayTargetHeight * _heightScale
clip: true clip: true
opacity: 0 opacity: 0
@ -192,6 +258,7 @@ Column {
return _notifCard.implicitHeight; return _notifCard.implicitHeight;
} }
// Animated version of _targetHeight smoothly transitions header height on collapse
property real _displayTargetHeight: _targetHeight property real _displayTargetHeight: _targetHeight
Behavior on _displayTargetHeight { Behavior on _displayTargetHeight {
NumberAnimation { NumberAnimation {
@ -238,6 +305,7 @@ Column {
id: _headerHover id: _headerHover
} }
// Tap target for collapse covers header row only, excludes dismiss button
Item { Item {
anchors.left: parent.left anchors.left: parent.left
anchors.right: _groupDismissBtn.left anchors.right: _groupDismissBtn.left
@ -248,7 +316,7 @@ Column {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
} }
TapHandler { TapHandler {
onTapped: root._toggleCollapse(notifDelegate.modelData.appName) onTapped: menuWindow._toggleCollapse(notifDelegate.modelData.appName)
} }
} }
@ -274,6 +342,7 @@ Column {
asynchronous: true asynchronous: true
} }
// Collapse chevron
Text { Text {
id: _chevron id: _chevron
anchors.right: _groupDismissBtn.left anchors.right: _groupDismissBtn.left
@ -288,6 +357,7 @@ Column {
opacity: _headerHover.hovered ? 1 : 0 opacity: _headerHover.hovered ? 1 : 0
} }
// App name
Text { Text {
anchors.left: _headerIcon.visible ? _headerIcon.right : parent.left anchors.left: _headerIcon.visible ? _headerIcon.right : parent.left
anchors.leftMargin: _headerIcon.visible ? 6 : 10 anchors.leftMargin: _headerIcon.visible ? 6 : 10
@ -304,6 +374,7 @@ Column {
elide: Text.ElideRight elide: Text.ElideRight
} }
// Dismiss button opacity-hidden when header not hovered
Text { Text {
id: _groupDismissBtn id: _groupDismissBtn
anchors.right: parent.right anchors.right: parent.right
@ -325,11 +396,12 @@ Column {
TapHandler { TapHandler {
onTapped: { onTapped: {
if (notifDelegate._type === "header") if (notifDelegate._type === "header")
root._cascadeGroupDismiss(notifDelegate.modelData.appName); menuWindow._cascadeGroupDismiss(notifDelegate.modelData.appName);
} }
} }
} }
// Collapsed preview: one line per notification summary
Repeater { Repeater {
model: (notifDelegate._type === "header" && notifDelegate.modelData.collapsed) ? notifDelegate.modelData.summaries : [] model: (notifDelegate._type === "header" && notifDelegate.modelData.collapsed) ? notifDelegate.modelData.summaries : []
@ -353,6 +425,7 @@ Column {
} }
// ---- Individual notification ---- // ---- Individual notification ----
M.NotifCard { M.NotifCard {
id: _notifCard id: _notifCard
visible: notifDelegate._type === "notif" visible: notifDelegate._type === "notif"
@ -372,7 +445,7 @@ Column {
NumberAnimation { NumberAnimation {
target: notifDelegate target: notifDelegate
property: "x" property: "x"
to: root.contentWidth to: menuWindow.contentWidth
duration: 200 duration: 200
easing.type: Easing.InCubic easing.type: Easing.InCubic
} }
@ -404,7 +477,7 @@ Column {
// Empty state // Empty state
Text { Text {
visible: S.NotifService.count === 0 visible: S.NotifService.count === 0
width: root.contentWidth width: menuWindow.contentWidth
height: 48 height: 48
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter

View file

@ -3,32 +3,21 @@ import Quickshell
import Quickshell.Services.Notifications import Quickshell.Services.Notifications
import "." as M import "." as M
import "../services" as S import "../services" as S
import "../applets" as C
M.BarSection { M.BarSection {
id: root id: root
spacing: S.Theme.moduleSpacing spacing: S.Theme.moduleSpacing
tooltip: "" tooltip: {
const parts = [S.NotifService.count + " notification" + (S.NotifService.count !== 1 ? "s" : "")];
if (S.NotifService.dnd)
parts.push("Do not disturb");
return parts.join("\n");
}
required property var bar
readonly property bool hasUrgent: S.NotifService.list.some(n => n.urgency === NotificationUrgency.Critical && n.state !== "dismissed") readonly property bool hasUrgent: S.NotifService.list.some(n => n.urgency === NotificationUrgency.Critical && n.state !== "dismissed")
property bool _pinned: false
readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered
readonly property bool _showPanel: _anyHover || _pinned
on_AnyHoverChanged: {
if (_anyHover)
_unpinTimer.stop();
else if (_pinned)
_unpinTimer.start();
}
Timer {
id: _unpinTimer
interval: 500
onTriggered: root._pinned = false
}
M.BarIcon { M.BarIcon {
icon: { icon: {
if (S.NotifService.dnd) if (S.NotifService.dnd)
@ -37,18 +26,12 @@ M.BarSection {
} }
color: S.NotifService.dnd ? S.Theme.base04 : root.accentColor color: S.NotifService.dnd ? S.Theme.base04 : root.accentColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
} }
M.BarLabel { M.BarLabel {
id: countLabel id: countLabel
label: S.NotifService.count > 0 ? String(S.NotifService.count) + (root.hasUrgent ? "!" : "") : "" label: S.NotifService.count > 0 ? String(S.NotifService.count) + (root.hasUrgent ? "!" : "") : ""
color: root.hasUrgent ? S.Theme.base08 : root.accentColor color: root.hasUrgent ? S.Theme.base08 : root.accentColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler {
onTapped: root._pinned = !root._pinned
}
transform: Scale { transform: Scale {
id: countScale id: countScale
@ -85,66 +68,26 @@ M.BarSection {
} }
} }
// Right-click DND quick toggle TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: {
centerLoader.active = !centerLoader.active;
M.FlyoutState.visible = false;
}
}
TapHandler { TapHandler {
acceptedButtons: Qt.RightButton acceptedButtons: Qt.RightButton
onTapped: S.NotifService.toggleDnd() onTapped: S.NotifService.toggleDnd()
} }
M.HoverPanel { LazyLoader {
id: hoverPanel id: centerLoader
showPanel: root._showPanel active: false
screen: QsWindow.window?.screen ?? null M.NotifCenter {
anchorItem: root
accentColor: root.accentColor
panelNamespace: "nova-notifications"
panelTitle: "Notifications"
contentWidth: 350
titleActionsComponent: Component {
Row {
spacing: 8
// DND toggle
Text {
text: S.NotifService.dnd ? "\uDB82\uDE93" : "\uDB80\uDC9C"
color: S.NotifService.dnd ? S.Theme.base09 : S.Theme.base04
font.pixelSize: S.Theme.fontSize
font.family: S.Theme.iconFontFamily
anchors.verticalCenter: parent.verticalCenter
HoverHandler {
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: S.NotifService.toggleDnd()
}
}
// Clear all
Text {
text: "\uF1F8"
color: _clearHover.hovered ? S.Theme.base08 : S.Theme.base04
font.pixelSize: S.Theme.fontSize
font.family: S.Theme.iconFontFamily
anchors.verticalCenter: parent.verticalCenter
visible: S.NotifService.count > 0
HoverHandler {
id: _clearHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: _notifApplet.cascadeDismiss()
}
}
}
}
C.NotifApplet {
id: _notifApplet
width: hoverPanel.contentWidth
contentWidth: hoverPanel.contentWidth
accentColor: root.accentColor accentColor: root.accentColor
screen: root.bar.screen
anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0)
onDismissed: centerLoader.active = false
} }
} }
} }

View file

@ -20,6 +20,7 @@ MemoryModule 1.0 MemoryModule.qml
MprisModule 1.0 MprisModule.qml MprisModule 1.0 MprisModule.qml
NetworkModule 1.0 NetworkModule.qml NetworkModule 1.0 NetworkModule.qml
NotifCard 1.0 NotifCard.qml NotifCard 1.0 NotifCard.qml
NotifCenter 1.0 NotifCenter.qml
NotifPopup 1.0 NotifPopup.qml NotifPopup 1.0 NotifPopup.qml
NotificationsModule 1.0 NotificationsModule.qml NotificationsModule 1.0 NotificationsModule.qml
OverviewBackdrop 1.0 OverviewBackdrop.qml OverviewBackdrop 1.0 OverviewBackdrop.qml

View file

@ -99,8 +99,7 @@ QtObject {
notifications: true, notifications: true,
mpris: true, mpris: true,
volume: true, volume: true,
weather: true, weather: true
threatEffect: true
}) })
property var statsDaemon: ({ property var statsDaemon: ({
interval: -1 interval: -1