diff --git a/config/dot/hypr/DESKTOP-PARITY.md b/config/dot/hypr/DESKTOP-PARITY.md index 059f55d..99417c5 100644 --- a/config/dot/hypr/DESKTOP-PARITY.md +++ b/config/dot/hypr/DESKTOP-PARITY.md @@ -20,7 +20,8 @@ Last live audit: 2026-08-17, Fedora 44, Hyprland 0.56.2, Quickshell 0.3.0. | Panel | Workspaces, CPU/RAM/GPU, clock, weather, quiet event capsule, media, clipboard, tray, status | Live | | Clipboard history | Vicinae owns capture/storage; Quickshell provides the panel | Live | | Notifications | Quickshell server, toasts, grouped history, DND, clear actions | Live | -| Date menu | Two-column Daybook with EDS agenda, notifications, month calendar, weather, and media | Live | +| Date menu | Two-column Daybook with Agenda, Ongoing activity, Notifications, month calendar, weather, and media | Live | +| Ongoing activity | Focus, Caffeine, recording, screen sharing, camera, and microphone state with direct controls | Live | | Calendar agenda | Existing GNOME, Google, iCloud, and Nextcloud sources through Evolution Data Server | Live | | Quick settings | Wi-Fi, Bluetooth, audio input/output, caffeine, night light, focus | Live | | Focus sessions | Persistent workspace-bound timer, DND/caffeine coordination, Signal Glass controls | Live | diff --git a/config/dot/hypr/README.md b/config/dot/hypr/README.md index 8fc917c..bbe1cd4 100644 --- a/config/dot/hypr/README.md +++ b/config/dot/hypr/README.md @@ -191,8 +191,16 @@ Settings remains searchable in Vicinae for hardware and account panels. Click the clock to open the two-column Daybook on **Agenda**. The month grid, today's schedule, Up Next, weather, and conditional media controls stay visible -together. `SUPER + B` opens **Notifications** in that same surface; returning to -Agenda preserves the selected date and visible month. +together. Its right pane has three first-class pages: + +- **Agenda** — the selected day's calendar. +- **Ongoing** — Focus, Caffeine, recording, screen sharing, camera, and + microphone activity, visible until each state ends. +- **Notifications** — grouped history, Do Not Disturb, and clear actions. + +`SUPER + B` opens Notifications directly. Switching pages preserves the +selected date and visible month. Brief completed events such as screenshots and +device changes remain in Signal Glass instead of accumulating in Ongoing. Calendar data comes from Evolution Data Server, so the Google, iCloud, Nextcloud, and local calendars already configured for GNOME remain the source diff --git a/config/dot/quickshell/modules/datemenu/AgendaPane.qml b/config/dot/quickshell/modules/datemenu/AgendaPane.qml index 301dbb5..7658aa1 100644 --- a/config/dot/quickshell/modules/datemenu/AgendaPane.qml +++ b/config/dot/quickshell/modules/datemenu/AgendaPane.qml @@ -7,8 +7,6 @@ import qs.modules.quicksettings Item { id: root - signal requestNotifications - readonly property bool isToday: CalendarAgenda.selectedDateKey === CalendarAgenda.dateKey(clock.date) readonly property var upNext: CalendarAgenda.nextEventForSelectedDate readonly property var listedEvents: CalendarAgenda.selectedEvents.filter(event => !root.upNext || event.id !== root.upNext.id) @@ -238,10 +236,6 @@ Item { } } - NotificationSummary { - width: parent.width - onActivated: root.requestNotifications() - } } function relativeStart(event: var): string { diff --git a/config/dot/quickshell/modules/datemenu/DateMenu.qml b/config/dot/quickshell/modules/datemenu/DateMenu.qml index f353dd9..84acff6 100644 --- a/config/dot/quickshell/modules/datemenu/DateMenu.qml +++ b/config/dot/quickshell/modules/datemenu/DateMenu.qml @@ -1,4 +1,4 @@ -// GNOME's date/time menu: the calendar and the message tray in one panel, +// GNOME's date/time menu: calendar, ongoing activity and messages in one panel, // dropping from the top centre because that is where GNOME put it and the // muscle memory is the whole point. // @@ -20,8 +20,10 @@ import qs.widgets PanelWindow { id: root - // "notifications" is the existing overlay key, so Super+B, the notification - // IPC target and the clock all land here. + readonly property bool ongoingContentFits: panel.ongoingContentFits + + // "notifications" remains the overlay key so existing keybinds keep their + // contract; the surface itself now has Agenda, Ongoing and Notifications. visible: ShellState.notificationsOpen color: "transparent" diff --git a/config/dot/quickshell/modules/datemenu/DateMenuPanel.qml b/config/dot/quickshell/modules/datemenu/DateMenuPanel.qml index 585511a..ef4d14a 100644 --- a/config/dot/quickshell/modules/datemenu/DateMenuPanel.qml +++ b/config/dot/quickshell/modules/datemenu/DateMenuPanel.qml @@ -1,5 +1,5 @@ // The approved A · Daybook: month context on the left, the selected day's -// schedule or grouped notification history on the right. +// schedule, persistent activity or grouped notification history on the right. import QtQuick import Quickshell @@ -46,7 +46,7 @@ Item { Text { anchors.left: parent.left anchors.leftMargin: 4 - anchors.right: headerActions.left + anchors.right: dateMenuTabs.left anchors.rightMargin: Theme.itemSpacing anchors.verticalCenter: parent.verticalCenter text: Qt.formatDateTime(clock.date, "dddd, MMMM d") @@ -59,35 +59,36 @@ Item { } Row { - id: headerActions + id: dateMenuTabs anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: 6 HeaderPill { - visible: ShellState.dateMenuPage === "notifications" - text: "← Back to agenda" + text: "Agenda" + active: ShellState.dateMenuPage === "agenda" onClicked: ShellState.dateMenuPage = "agenda" } HeaderPill { - text: "Do Not Disturb" - active: Notifs.doNotDisturb - icon: "notifications-disabled-symbolic" - onClicked: Notifs.doNotDisturb = !Notifs.doNotDisturb + text: "Ongoing" + active: ShellState.dateMenuPage === "ongoing" + badge: Ongoing.count + onClicked: ShellState.dateMenuPage = "ongoing" + } + + HeaderPill { + text: "Notifications" + active: ShellState.dateMenuPage === "notifications" + badge: Notifs.history.length + onClicked: ShellState.dateMenuPage = "notifications" } HeaderPill { visible: ShellState.dateMenuPage === "agenda" - text: "Open Calendar ↗" + text: "Calendar ↗" onClicked: CalendarAgenda.openCalendar(CalendarAgenda.selectedDate) } - - HeaderPill { - visible: ShellState.dateMenuPage === "notifications" && Notifs.hasNotifications - text: "Clear all" - onClicked: Notifs.dismissAll() - } } } @@ -165,10 +166,16 @@ Item { anchors.top: parent.top anchors.margins: 14 visible: ShellState.dateMenuPage === "agenda" - onRequestNotifications: { - ShellState.dateMenuPage = "notifications"; - Notifs.markAllRead(); - } + } + + OngoingPane { + id: ongoingPane + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: 14 + maxHeight: body.height - 28 + visible: ShellState.dateMenuPage === "ongoing" } Column { @@ -186,6 +193,8 @@ Item { Text { anchors.left: parent.left + anchors.right: notificationActions.left + anchors.rightMargin: 8 anchors.verticalCenter: parent.verticalCenter text: "Notifications" color: Theme.fg @@ -194,14 +203,24 @@ Item { font.weight: Font.DemiBold } - Text { + Row { + id: notificationActions anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - text: Notifs.history.length + (Notifs.history.length === 1 ? " item" : " items") - color: Theme.fgDim - font.family: Theme.fontFamily - font.features: Theme.tabularFigures - font.pixelSize: Theme.fontSizeSmall + spacing: 6 + + HeaderPill { + text: "Do Not Disturb" + active: Notifs.doNotDisturb + icon: "notifications-disabled-symbolic" + onClicked: Notifs.doNotDisturb = !Notifs.doNotDisturb + } + + HeaderPill { + visible: Notifs.hasNotifications + text: "Clear all" + onClicked: Notifs.dismissAll() + } } } @@ -215,9 +234,14 @@ Item { } } - readonly property real rightContentHeight: ShellState.dateMenuPage === "notifications" - ? notificationPane.implicitHeight - : agenda.implicitHeight + readonly property real rightContentHeight: { + if (ShellState.dateMenuPage === "notifications") + return notificationPane.implicitHeight; + if (ShellState.dateMenuPage === "ongoing") + return ongoingPane.implicitHeight; + return agenda.implicitHeight; + } + readonly property bool ongoingContentFits: ongoingPane.implicitHeight <= body.height - 28 component HeaderPill: Rectangle { id: pill @@ -225,6 +249,7 @@ Item { required property string text property string icon: "" property bool active: false + property int badge: -1 signal clicked width: pillRow.implicitWidth + 20 @@ -260,6 +285,27 @@ Item { font.pixelSize: Theme.fontSizeSmall font.weight: Font.Medium } + + Rectangle { + anchors.verticalCenter: parent.verticalCenter + visible: pill.badge > 0 + width: visible ? badgeText.implicitWidth + 9 : 0 + height: 17 + radius: Theme.pillRadius + border.width: 0 + color: Theme.alpha(pill.active ? Theme.accent : Theme.fg, 0.12) + + Text { + id: badgeText + anchors.centerIn: parent + text: String(pill.badge) + color: pill.active ? Theme.accent : Theme.fgDim + font.family: Theme.fontFamily + font.features: Theme.tabularFigures + font.pixelSize: 9 + font.weight: Font.DemiBold + } + } } MouseArea { diff --git a/config/dot/quickshell/modules/datemenu/OngoingPane.qml b/config/dot/quickshell/modules/datemenu/OngoingPane.qml new file mode 100644 index 0000000..d5ed3ae --- /dev/null +++ b/config/dot/quickshell/modules/datemenu/OngoingPane.qml @@ -0,0 +1,263 @@ +// Persistent work and privacy activity inside the Daybook. + +import QtQuick +import qs.config +import qs.services +import qs.modules.quicksettings + +Item { + id: root + + property real maxHeight: 500 + implicitHeight: content.implicitHeight + + Column { + id: content + width: parent.width + spacing: 12 + + Item { + width: parent.width + height: 38 + + Column { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + Text { + text: "PERSISTENT ACTIVITY" + color: Theme.fgMuted + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.DemiBold + font.letterSpacing: 0.9 + } + Text { + text: "Ongoing" + color: Theme.fg + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.DemiBold + } + } + + Text { + anchors.right: parent.right + anchors.bottom: parent.bottom + text: Ongoing.count === 0 ? "Nothing active" : Ongoing.count + (Ongoing.count === 1 ? " activity" : " activities") + color: Theme.fgDim + font.family: Theme.fontFamily + font.features: Theme.tabularFigures + font.pixelSize: Theme.fontSizeSmall + } + } + + Item { + width: parent.width + height: visible ? 190 : 0 + visible: Ongoing.count === 0 + + Column { + anchors.centerIn: parent + spacing: 8 + + Text { + anchors.horizontalCenter: parent.horizontalCenter + text: "\u{F04A5}" + color: Theme.fgMuted + font.family: Theme.fontMono + font.pixelSize: 24 + } + Text { + anchors.horizontalCenter: parent.horizontalCenter + text: "Nothing ongoing" + color: Theme.fg + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize + font.weight: Font.DemiBold + } + Text { + anchors.horizontalCenter: parent.horizontalCenter + text: "Focus, Caffeine, recording and privacy activity appear here." + color: Theme.fgDim + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + } + } + } + + ScrollColumn { + width: parent.width + // Header + footer + the two 12px Column gaps consume 116px. + // Keeping that outside the scroll budget prevents the pane from + // growing beyond the Daybook when every producer is active. + maxHeight: Math.max(0, root.maxHeight - 116) + spacing: 8 + visible: Ongoing.count > 0 + + Repeater { + model: Ongoing.activities + + Rectangle { + id: activityRow + required property var modelData + + width: parent.width + height: 62 + radius: Theme.cardRadius + border.width: 0 + color: rowMouse.containsMouse + ? Theme.alpha(Theme.fg, 0.10) + : Theme.alpha(Theme.fg, 0.055) + + Behavior on color { ColorAnimation { duration: Theme.durFast } } + + Text { + id: activityIcon + anchors.left: parent.left + anchors.leftMargin: 13 + anchors.verticalCenter: parent.verticalCenter + text: activityRow.modelData.glyph + color: root.toneColor(activityRow.modelData.tone) + font.family: Theme.fontMono + font.pixelSize: 17 + } + + Column { + anchors.left: activityIcon.right + anchors.leftMargin: 11 + anchors.right: activityAction.visible ? activityAction.left : activityState.left + anchors.rightMargin: 10 + anchors.verticalCenter: parent.verticalCenter + spacing: 3 + + Text { + width: parent.width + text: activityRow.modelData.label + color: Theme.fg + elide: Text.ElideRight + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize + font.weight: Font.DemiBold + } + Text { + width: parent.width + text: activityRow.modelData.detail + color: Theme.fgDim + elide: Text.ElideRight + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + } + } + + Rectangle { + id: activityState + anchors.right: activityAction.visible ? activityAction.left : parent.right + anchors.rightMargin: activityAction.visible ? 7 : 12 + anchors.verticalCenter: parent.verticalCenter + width: stateLabel.implicitWidth + 16 + height: 25 + radius: Theme.pillRadius + border.width: 0 + color: Theme.alpha(root.toneColor(activityRow.modelData.tone), 0.12) + + Text { + id: stateLabel + anchors.centerIn: parent + text: activityRow.modelData.state + color: root.toneColor(activityRow.modelData.tone) + font.family: Theme.fontFamily + font.features: Theme.tabularFigures + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.DemiBold + } + } + + Rectangle { + id: activityAction + z: 2 + anchors.right: parent.right + anchors.rightMargin: 9 + anchors.verticalCenter: parent.verticalCenter + visible: activityRow.modelData.action !== "" + width: visible ? actionLabel.implicitWidth + 18 : 0 + height: 28 + radius: Theme.pillRadius + border.width: 0 + color: actionMouse.containsMouse + ? Theme.alpha(activityRow.modelData.tone === "danger" ? Theme.danger : Theme.fg, 0.20) + : Theme.alpha(activityRow.modelData.tone === "danger" ? Theme.danger : Theme.fg, 0.08) + + Text { + id: actionLabel + anchors.centerIn: parent + text: activityRow.modelData.action + color: activityRow.modelData.tone === "danger" ? Theme.danger : Theme.fg + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + } + + MouseArea { + id: actionMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Ongoing.invoke(activityRow.modelData.kind) + } + } + + MouseArea { + id: rowMouse + anchors.fill: parent + z: 1 + enabled: activityRow.modelData.kind !== "caffeine" + hoverEnabled: enabled + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: Ongoing.activate(activityRow.modelData.kind) + } + } + } + } + + Rectangle { + width: parent.width + height: 54 + radius: Theme.cardRadius + border.width: 0 + visible: Ongoing.count > 0 + color: Theme.alpha(Theme.fg, 0.035) + + Column { + anchors.left: parent.left + anchors.leftMargin: 13 + anchors.verticalCenter: parent.verticalCenter + spacing: 3 + Text { + text: "Visible until it ends" + color: Theme.fg + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.DemiBold + } + Text { + text: "Completed screenshots, devices and other brief events stay in Signal Glass." + color: Theme.fgDim + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + } + } + } + } + + function toneColor(tone: string): color { + if (tone === "danger") + return Theme.danger; + if (tone === "warn") + return Theme.warn; + if (tone === "ok") + return Theme.ok; + return Theme.accent; + } +} diff --git a/config/dot/quickshell/modules/datemenu/qmldir b/config/dot/quickshell/modules/datemenu/qmldir index b7967a4..730e105 100644 --- a/config/dot/quickshell/modules/datemenu/qmldir +++ b/config/dot/quickshell/modules/datemenu/qmldir @@ -9,4 +9,5 @@ MonthArrow 1.0 MonthArrow.qml NotificationGroup 1.0 NotificationGroup.qml NotificationList 1.0 NotificationList.qml NotificationSummary 1.0 NotificationSummary.qml +OngoingPane 1.0 OngoingPane.qml WeatherCard 1.0 WeatherCard.qml diff --git a/config/dot/quickshell/services/Ongoing.qml b/config/dot/quickshell/services/Ongoing.qml new file mode 100644 index 0000000..1ae43f2 --- /dev/null +++ b/config/dot/quickshell/services/Ongoing.qml @@ -0,0 +1,98 @@ +pragma Singleton + +// One presentation model for state that remains active until it is explicitly +// stopped or naturally completes. Transient confirmations stay in Signal Glass. + +import Quickshell +import QtQuick + +Singleton { + id: root + + readonly property var activities: { + const result = []; + + if (FocusSession.active) { + result.push({ + kind: "focus", + glyph: "\u{F051F}", + label: "Focus session", + detail: (FocusSession.workspaceLabel || "Focused workspace") + " · " + FocusSession.statusText, + state: FocusSession.remainingText, + tone: FocusSession.paused ? "warn" : "accent", + action: FocusSession.paused ? "Resume" : "Pause" + }); + } + + if (Caffeine.enabled) { + result.push({ + kind: "caffeine", + glyph: "\u{F0F2E}", + label: "Caffeine", + detail: "Automatic sleep is blocked", + state: "On", + tone: "ok", + action: "Turn off" + }); + } + + if (PrivacyState.recordingActive) { + result.push({ + kind: "recording", + glyph: "\u{F044A}", + label: "Screen recording", + detail: "Panama · " + root.recordingElapsed, + state: "Live", + tone: "danger", + action: "Stop" + }); + } + if (PrivacyState.screenSharingActive) + result.push(root.privacyActivity("screen", "\u{F0379}", "Screen sharing", PrivacyState.screenSharingApp)); + if (PrivacyState.cameraActive) + result.push(root.privacyActivity("camera", "\u{F0100}", "Camera", PrivacyState.cameraApp)); + if (PrivacyState.microphoneActive) + result.push(root.privacyActivity("microphone", "\u{F036C}", "Microphone", PrivacyState.microphoneApp)); + + return result; + } + + readonly property int count: activities.length + readonly property string recordingElapsed: { + const total = Capture.recordingSeconds; + const seconds = String(total % 60).padStart(2, "0"); + const minutes = Math.floor(total / 60) % 60; + const hours = Math.floor(total / 3600); + return hours > 0 + ? `${hours}:${String(minutes).padStart(2, "0")}:${seconds}` + : `${minutes}:${seconds}`; + } + + function privacyActivity(kind: string, glyph: string, label: string, app: string): var { + return { + kind, + glyph, + label, + detail: app || "Managed by the application", + state: "In use", + tone: "warn", + action: "" + }; + } + + function activate(kind: string): void { + if (kind === "focus") + FocusSession.activateWorkspace(); + else if (["recording", "screen", "camera", "microphone"].indexOf(kind) >= 0) + ShellState.open("activity"); + } + + function invoke(kind: string): void { + if (kind === "focus") + FocusSession.pauseOrResume(); + else if (kind === "caffeine") + Caffeine.toggle(); + else if (kind === "recording") + Capture.stopRecording(); + } +} diff --git a/config/dot/quickshell/services/PrivacyState.qml b/config/dot/quickshell/services/PrivacyState.qml index c024fc8..20a79d2 100644 --- a/config/dot/quickshell/services/PrivacyState.qml +++ b/config/dot/quickshell/services/PrivacyState.qml @@ -138,6 +138,8 @@ Singleton { root.applyStates(false, true, false, "", "Contract fixture", "", true); else if (name === "screen-on") root.applyStates(false, false, true, "", "", "Contract fixture", true); + else if (name === "all-on") + root.applyStates(true, true, true, "Contract fixture", "Contract fixture", "Contract fixture", true); else if (name === "all-off") root.applyStates(false, false, false, "", "", "", true); } diff --git a/config/dot/quickshell/services/ShellState.qml b/config/dot/quickshell/services/ShellState.qml index ec96d55..dd503de 100644 --- a/config/dot/quickshell/services/ShellState.qml +++ b/config/dot/quickshell/services/ShellState.qml @@ -35,8 +35,8 @@ Singleton { readonly property bool anyOverlayOpen: activeOverlay !== "" - // The date menu is one surface with two deliberate entry points. The clock - // opens the Daybook; Super+B and notification actions open message history. + // The date menu keeps calendar context fixed while its right pane switches + // between the schedule, persistent activity and message history. property string dateMenuPage: "agenda" // 0 means "use the currently focused workspace". A positive value lets a @@ -53,12 +53,12 @@ Singleton { } function openDateMenu(page: string): void { - root.dateMenuPage = page === "notifications" ? "notifications" : "agenda"; + root.dateMenuPage = root.normalizedDateMenuPage(page); root.activeOverlay = "notifications"; } function toggleDateMenu(page: string): void { - const target = page === "notifications" ? "notifications" : "agenda"; + const target = root.normalizedDateMenuPage(page); if (root.activeOverlay === "notifications" && root.dateMenuPage === target) { root.activeOverlay = ""; return; @@ -67,6 +67,10 @@ Singleton { root.activeOverlay = "notifications"; } + function normalizedDateMenuPage(page: string): string { + return ["agenda", "ongoing", "notifications"].indexOf(page) >= 0 ? page : "agenda"; + } + function openOverview(workspaceId: int): void { root.overviewWorkspaceId = Math.max(0, workspaceId); root.overviewQuery = ""; diff --git a/config/dot/quickshell/shell.qml b/config/dot/quickshell/shell.qml index 8b870a9..c8bc237 100644 --- a/config/dot/quickshell/shell.qml +++ b/config/dot/quickshell/shell.qml @@ -79,7 +79,7 @@ ShellRoot { Overview {} // QuickSettings is the window; QuickSettingsPanel is its content. QuickSettings {} - DateMenu {} + DateMenu { id: dateMenu } ClipboardPanel {} CaptureOverlay {} IntelligenceResult {} @@ -268,6 +268,7 @@ ShellRoot { function reset(): void { CalendarAgenda.clearFixture(); } function refresh(): void { CalendarAgenda.refresh(); } function open(): void { ShellState.openDateMenu("agenda"); } + function page(name: string): void { ShellState.openDateMenu(name); } function close(): void { ShellState.close(); } function select(date: string): void { CalendarAgenda.selectIsoDate(date); } function status(): string { @@ -281,7 +282,9 @@ ShellRoot { selectedDate: CalendarAgenda.selectedDateKey, selectedEventCount: CalendarAgenda.selectedEvents.length, capsuleVisible: CalendarAgenda.capsuleVisible, - capsuleText: CalendarAgenda.capsuleText + capsuleText: CalendarAgenda.capsuleText, + ongoingCount: Ongoing.count, + ongoingFits: dateMenu.ongoingContentFits }); } } diff --git a/tests/quickshell/calendar-agenda-contract.sh b/tests/quickshell/calendar-agenda-contract.sh index 21e7e2a..7cab30a 100755 --- a/tests/quickshell/calendar-agenda-contract.sh +++ b/tests/quickshell/calendar-agenda-contract.sh @@ -8,6 +8,8 @@ fail() { } cleanup() { + qs ipc call focus end >/dev/null 2>&1 || true + qs ipc call activity clear >/dev/null 2>&1 || true qs ipc call calendar-agenda reset >/dev/null 2>&1 || true qs ipc call calendar-agenda close >/dev/null 2>&1 || true } @@ -40,6 +42,18 @@ jq -e '.open == true and .page == "notifications"' \ <<<"$(qs ipc call calendar-agenda status)" >/dev/null \ || fail 'notification open did not route to Notifications' +qs ipc call activity fixture microphone-on >/dev/null +qs ipc call calendar-agenda page ongoing >/dev/null +jq -e '.open == true and .page == "ongoing" and .ongoingCount >= 1' \ + <<<"$(qs ipc call calendar-agenda status)" >/dev/null \ + || fail 'persistent activity did not route to Ongoing' + +qs ipc call activity fixture all-on >/dev/null +qs ipc call focus start >/dev/null +jq -e '.ongoingCount == 5 and .ongoingFits == true' \ + <<<"$(qs ipc call calendar-agenda status)" >/dev/null \ + || fail 'crowded Ongoing activity does not fit the Daybook' + hyprctl layers | rg -q 'namespace: qs-popover-datemenu' \ || fail 'Daybook layer did not map' rg -q 'implicitWidth: 760' config/dot/quickshell/modules/datemenu/DateMenu.qml \