diff --git a/flake.lock b/flake.lock index 531d7a71..8fd68aae 100644 --- a/flake.lock +++ b/flake.lock @@ -250,11 +250,11 @@ ] }, "locked": { - "lastModified": 1790128814, - "narHash": "sha256-6Gm9q+wW3E4Ey4F6wEbJAwaMsEK6hvCYfTW7yY64ZfA=", + "lastModified": 1790292277, + "narHash": "sha256-z+knvdMItRrMonmG4RIsqKYIa/hwYUzKjotDTfC5DDc=", "owner": "nix-community", "repo": "home-manager", - "rev": "0b2f1129177f70c5f0f5d88bb53c49ca47d0bfc0", + "rev": "4b9add8645d5e2b0f7de18f7a442d08fe2bdcc91", "type": "github" }, "original": { @@ -339,11 +339,11 @@ "xdph": "xdph" }, "locked": { - "lastModified": 1790080419, - "narHash": "sha256-ltWq63fW4EsgkSS0w7H2cdjsQhlQ5v0JzTXFM4K5aTA=", + "lastModified": 1790173459, + "narHash": "sha256-GR2NqjxK1C+Z2JxuwmpyaSvTwBCuKFqiznuQKRfSLYQ=", "ref": "refs/heads/main", - "rev": "23118f9f7f24db7447069949c2df7fcd8ba380d0", - "revCount": 7827, + "rev": "e368c13c27a42a173b9e08fa0bf413f9f7073187", + "revCount": 7828, "submodules": true, "type": "git", "url": "https://github.com/hyprwm/Hyprland" @@ -587,11 +587,11 @@ }, "mnw": { "locked": { - "lastModified": 1780772958, - "narHash": "sha256-VKKe8r4pwCGWZ3Yr9CPN129R4S3CKLSrlYqdYz3vKpM=", + "lastModified": 1784154424, + "narHash": "sha256-HRn4O1X2ShYtbq5Egji72/aemyc2cIQicjRZD9hBqM8=", "owner": "Gerg-L", "repo": "mnw", - "rev": "0871dbf63a53610c95db04439ed8ea4d6ec9c160", + "rev": "0151d9fa87992cbeb67c86616fb9170dcec830b6", "type": "github" }, "original": { @@ -765,11 +765,11 @@ }, "nixpkgs_3": { "locked": { - "lastModified": 1790046670, - "narHash": "sha256-MYiI+CzL0tuWgRPjGsKCDHqYs2T3OzMlMQWOYWG0qso=", + "lastModified": 1790185690, + "narHash": "sha256-xJ+X4hBtOcAFGBOe5nAMyMUeF9foJBmIOu3NjBqBycU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "6774f7bc253789b113a4f39285dc0fa100abeacc", + "rev": "4975466d324710c576dc11ad614684e6bd8cad8e", "type": "github" }, "original": { @@ -788,11 +788,11 @@ ] }, "locked": { - "lastModified": 1789854559, - "narHash": "sha256-6GDssza3F+ukpUujSuTL50vW5AHaoG+4vjsDgWqRyeQ=", + "lastModified": 1790221470, + "narHash": "sha256-TzyWd4i7YCpqoF9xB8mfgDJ2sviAZysKH9R8YbQGjqA=", "owner": "notashelf", "repo": "nvf", - "rev": "85bf39cd0409ad8a7fa5b3ad0d29e89e9da97a0e", + "rev": "95afe7a794216f53e217c5148c6fbe0b668192cf", "type": "github" }, "original": { @@ -847,11 +847,11 @@ ] }, "locked": { - "lastModified": 1789890976, - "narHash": "sha256-GKwH3zpy7tartuJMG0Rv/xUsdetG1QLmTVv8UKgJLmA=", + "lastModified": 1790247521, + "narHash": "sha256-TNfgoHsqsYYvaJImlWctfRkh4PseTagzegU1Dgdbehw=", "owner": "Mic92", "repo": "sops-nix", - "rev": "7214124c20c1542c90deb54af50e2f53ae02711f", + "rev": "2bd00bd9bb35fe6d114888c8f1c2e946c541dd8f", "type": "github" }, "original": { @@ -979,11 +979,11 @@ ] }, "locked": { - "lastModified": 1790160881, - "narHash": "sha256-VTHBBI0EvApXSssCthrYLsrGN4/0nM/tW9A17NEmKEU=", + "lastModified": 1790234305, + "narHash": "sha256-zgshjjI5uhmRQor+bmXH4EYx/E0LUdJEqv0L8WnTFOw=", "owner": "0xc000022070", "repo": "zen-browser-flake", - "rev": "1b16ef2b81078b5f363e6bb689d2193f8eccadb7", + "rev": "2b7fec6557495d97736f1b31904f79312671ca2a", "type": "github" }, "original": { diff --git a/modules/features/core/boot.nix b/modules/features/core/boot.nix index 2ded8d8e..c39c97ed 100644 --- a/modules/features/core/boot.nix +++ b/modules/features/core/boot.nix @@ -31,6 +31,7 @@ in enable = true; efiSupport = true; device = "nodev"; + useOSProber = true; dedsec-theme = { enable = true; style = "legion"; diff --git a/modules/features/desktop/hyprland.nix b/modules/features/desktop/hyprland.nix index 7c315b00..ad8ea276 100644 --- a/modules/features/desktop/hyprland.nix +++ b/modules/features/desktop/hyprland.nix @@ -45,7 +45,6 @@ let hl.on("hyprland.start", function() hl.exec_cmd("dbus-update-activation-environment --systemd DISPLAY HYPRLAND_INSTANCE_SIGNATURE WAYLAND_DISPLAY XDG_CURRENT_DESKTOP XDG_SESSION_TYPE && systemctl --user stop hyprland-session.target && systemctl --user start hyprland-session.target") hl.exec_cmd("systemctl --user start hyprpolkitagent") - hl.exec_cmd("ctos-shell") end) hl.config({ diff --git a/modules/features/desktop/niri.nix b/modules/features/desktop/niri.nix index 78f799e5..aa063243 100644 --- a/modules/features/desktop/niri.nix +++ b/modules/features/desktop/niri.nix @@ -52,9 +52,6 @@ let "systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP DISPLAY GTK_USE_PORTAL NIRI_SOCKET && systemctl --user start nixos-fake-graphical-session.target" ]; } - { - command = [ "ctos-shell" ]; - } { command = [ "hypridle" ]; } diff --git a/shell/desktop/core/Settings.qml b/shell/desktop/core/Settings.qml index 46f73cc5..a98d3d5c 100644 --- a/shell/desktop/core/Settings.qml +++ b/shell/desktop/core/Settings.qml @@ -38,6 +38,8 @@ Singleton { readonly property bool defaultWidgetTargetProfilerVisible: true property bool featuresCommandDeck: true property bool featuresNotifications: true + property int notificationCooldownSeconds: 30 + readonly property int defaultNotificationCooldownSeconds: 30 property bool featuresSystemRail: true // Indicator if configuration was successfully loaded from disk @@ -132,6 +134,14 @@ Singleton { featuresNotifications = defaultFeaturesNotifications; } + if (typeof data.notificationCooldownSeconds === "number" && Number.isInteger(data.notificationCooldownSeconds) && data.notificationCooldownSeconds >= 0) { + notificationCooldownSeconds = data.notificationCooldownSeconds; + } else if (data.features && typeof data.features.notificationCooldownSeconds === "number" && Number.isInteger(data.features.notificationCooldownSeconds) && data.features.notificationCooldownSeconds >= 0) { + notificationCooldownSeconds = data.features.notificationCooldownSeconds; + } else { + notificationCooldownSeconds = defaultNotificationCooldownSeconds; + } + // barHeight: supports flat key or nested bar.height if (typeof data.barHeight === "number" && Number.isInteger(data.barHeight) && data.barHeight > 0) { barHeight = data.barHeight; @@ -311,6 +321,7 @@ Singleton { data.featuresCommandDeck = root.featuresCommandDeck; data.featuresSystemRail = root.featuresSystemRail; data.featuresNotifications = root.featuresNotifications; + data.notificationCooldownSeconds = root.notificationCooldownSeconds; data.barHeight = root.barHeight; data.theme = root.theme; data.wallpaper = root.wallpaper; @@ -511,6 +522,7 @@ Singleton { featuresCommandDeck = defaultFeaturesCommandDeck; featuresSystemRail = defaultFeaturesSystemRail; featuresNotifications = defaultFeaturesNotifications; + notificationCooldownSeconds = defaultNotificationCooldownSeconds; barHeight = defaultBarHeight; theme = defaultTheme; wallpaper = defaultWallpaper; diff --git a/shell/desktop/services/NotificationService.qml b/shell/desktop/services/NotificationService.qml index 532f878a..c61c4e52 100644 --- a/shell/desktop/services/NotificationService.qml +++ b/shell/desktop/services/NotificationService.qml @@ -53,6 +53,9 @@ Singleton { // Map of notifId -> raw Notification object property var _notificationObjects: ({}) + // Map of "appName|summary" -> timestamp + property var _recentNotifications: ({}) + // Component for instantiating per-toast auto-expiry timers Component { id: toastTimerComponent @@ -178,6 +181,25 @@ Singleton { } catch (e) {} } + const dedupKey = appName + "|" + summary; + const now = Date.now(); + + // Clean up expired entries to prevent memory leak + const cutoff = now - Settings.notificationCooldownSeconds * 1000; + for (let key in root._recentNotifications) { + if (root._recentNotifications[key] < cutoff) { + delete root._recentNotifications[key]; + } + } + + let isDuplicate = false; + if (root._recentNotifications[dedupKey] !== undefined) { + if (now - root._recentNotifications[dedupKey] < Settings.notificationCooldownSeconds * 1000) { + isDuplicate = true; + } + } + root._recentNotifications[dedupKey] = now; + const record = { notifId: notifId, id: notifId, @@ -199,7 +221,7 @@ Singleton { } historyModel.insert(0, record); - if (!root.doNotDisturb) { + if (!root.doNotDisturb && !isDuplicate) { for (let t = activeToastsModel.count - 1; t >= 0; --t) { const existingToast = activeToastsModel.get(t); if (existingToast && (existingToast.notifId === notifId || existingToast.id === notifId)) { diff --git a/shell/desktop/surfaces/components/CalendarPopup.qml b/shell/desktop/surfaces/components/CalendarPopup.qml index 91310272..26f2d9a9 100644 --- a/shell/desktop/surfaces/components/CalendarPopup.qml +++ b/shell/desktop/surfaces/components/CalendarPopup.qml @@ -46,14 +46,30 @@ Rectangle { // Calendar State & Calculations // ========================================================================= - property int viewYear: (new Date()).getFullYear() - property int viewMonth: (new Date()).getMonth() + SystemClock { + id: systemClock + + precision: SystemClock.Minute + } + + property int todayYear: systemClock.date.getFullYear() + property int todayMonth: systemClock.date.getMonth() + property int todayDate: systemClock.date.getDate() + + property int viewYear: todayYear + property int viewMonth: todayMonth readonly property var monthNames: ["JANUARY", "FEBRUARY", "MARCH", "APRIL", "MAY", "JUNE", "JULY", "AUGUST", "SEPTEMBER", "OCTOBER", "NOVEMBER", "DECEMBER"] readonly property var dayHeaders: ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"] - readonly property var gridCells: generateCalendarCells(root.viewYear, root.viewMonth) + readonly property var gridCells: generateCalendarCells(root.viewYear, root.viewMonth, root.todayYear, root.todayMonth, root.todayDate) + + onVisibleChanged: { + if (visible) { + resetToToday(); + } + } function previousMonth(): void { if (viewMonth === 0) { @@ -74,17 +90,12 @@ Rectangle { } function resetToToday(): void { - const now = new Date(); - viewYear = now.getFullYear(); - viewMonth = now.getMonth(); + viewYear = Qt.binding(function() { return todayYear; }); + viewMonth = Qt.binding(function() { return todayMonth; }); } - function generateCalendarCells(year: int, month: int): var { + function generateCalendarCells(year: int, month: int, tYear: int, tMonth: int, tDate: int): var { const cells = []; - const today = new Date(); - const todayYear = today.getFullYear(); - const todayMonth = today.getMonth(); - const todayDate = today.getDate(); const firstDay = new Date(year, month, 1); const firstDayIndex = (firstDay.getDay() + 6) % 7; @@ -111,7 +122,7 @@ Rectangle { cellYear = month === 11 ? year + 1 : year; } - const isToday = (cellYear === todayYear && cellMonth === todayMonth && dayNum === todayDate && isCurrentMonth); + const isToday = (cellYear === tYear && cellMonth === tMonth && dayNum === tDate && isCurrentMonth); cells.push({ day: dayNum, @@ -361,7 +372,7 @@ Rectangle { color: Theme.textMuted font.family: Theme.fontFamilyMonospace font.pixelSize: Theme.fontSizeCaption - text: "// " + Qt.formatDateTime(new Date(), "yyyy-MM-dd") + text: "// " + Qt.formatDateTime(systemClock.date, "yyyy-MM-dd") } Rectangle { diff --git a/shell/greeter/common/components/Accents.qml b/shell/greeter/common/components/Accents.qml index 4652a95c..efcbe842 100644 --- a/shell/greeter/common/components/Accents.qml +++ b/shell/greeter/common/components/Accents.qml @@ -2,6 +2,8 @@ import QtQuick import qs.greeter.config +import Qt5Compat.GraphicalEffects + Item { id: root @@ -12,6 +14,8 @@ Item { property bool animate: true property int individualSize: 4 + property color tintColor: "transparent" + property int startingHorizontalOffset: 0 property int startingVerticalOffset: 0 @@ -44,6 +48,10 @@ Item { top: root.top topMargin: root._verticalOffset } + layer.enabled: !Qt.colorEqual(root.tintColor, "transparent") + layer.effect: ColorOverlay { + color: root.tintColor + } } Image { @@ -58,6 +66,10 @@ Item { bottomMargin: root._verticalOffset } rotation: 270 + layer.enabled: !Qt.colorEqual(root.tintColor, "transparent") + layer.effect: ColorOverlay { + color: root.tintColor + } } Image { @@ -72,6 +84,10 @@ Item { topMargin: root._verticalOffset } rotation: 90 + layer.enabled: !Qt.colorEqual(root.tintColor, "transparent") + layer.effect: ColorOverlay { + color: root.tintColor + } } Image { @@ -86,6 +102,10 @@ Item { bottomMargin: root._verticalOffset } rotation: 180 + layer.enabled: !Qt.colorEqual(root.tintColor, "transparent") + layer.effect: ColorOverlay { + color: root.tintColor + } } ParallelAnimation { diff --git a/shell/greeter/components/IdentityCard.qml b/shell/greeter/components/IdentityCard.qml index 8d9ea73f..f487460a 100644 --- a/shell/greeter/components/IdentityCard.qml +++ b/shell/greeter/components/IdentityCard.qml @@ -8,10 +8,15 @@ import qs.greeter.services Item { id: root + // ========================================================================= + // InfoField inline component + // ========================================================================= + component InfoField: Column { id: field property string label: "FIELD" property string value: "VALUE" + property color valueColor: Theme.textPrimary property alias fieldValueOpacity: fieldValue.opacity Text { @@ -20,99 +25,126 @@ Item { color: Theme.textPrimaryDim font { family: Settings.fontFamily - pixelSize: 14 + pixelSize: Math.round(11 * Units.vh) } } Text { id: fieldValue text: field.value - color: Theme.textPrimary + color: field.valueColor font { family: Settings.fontFamily - pixelSize: 22 + pixelSize: Math.round(17 * Units.vh) weight: 500 } + elide: Text.ElideRight } } - ColumnLayout { - width: parent.width * 0.55 - height: parent.height + // ========================================================================= + // Card layout — unified RowLayout splitting left info from right photo + // ========================================================================= - spacing: 15 + RowLayout { + anchors.fill: parent + spacing: 0 - RowLayout { - id: row - spacing: 10 + // ── Left column: fields + barcode ──────────────────────────────────── + ColumnLayout { + id: infoColumn + Layout.fillWidth: true + Layout.fillHeight: true + spacing: Math.round(4 * Units.vh) - InfoField { - id: employeeId + // Row 1: EMPID / CLASS / ACCESS + RowLayout { Layout.fillWidth: true - Layout.preferredWidth: 1 - label: "EMPID ##" - value: SessionManager.activeUser ? ("UID-" + SessionManager.activeUser.uid) : "UID-STANDBY" + spacing: Math.round(6 * Units.vh) + + InfoField { + id: employeeId + Layout.fillWidth: true + Layout.preferredWidth: 1 + label: "EMPID ##" + value: SessionManager.activeUser ? ("UID-" + SessionManager.activeUser.uid) : "UID-STANDBY" + } + InfoField { + id: employeeClass + Layout.fillWidth: true + Layout.preferredWidth: 1 + label: "CLASS" + value: { + if (!SessionManager.activeUser) { + return "STANDBY"; + } + if (SessionManager.activeUser.uid === 0) { + return "L0_ROOT"; + } + if (SessionManager.activeUser.uid === 1000) { + return "L5_ADMIN"; + } + return "OPERATOR"; + } + } + InfoField { + id: employeeAccess + Layout.fillWidth: true + Layout.preferredWidth: 1 + label: "ACCESS" + value: "RESTRICTED" + valueColor: Theme.accentRed + } } + + // Row 2: Full name InfoField { - id: employeeClass + id: employeeName Layout.fillWidth: true - Layout.preferredWidth: 1 - label: "CLASS" - value: { - if (!SessionManager.activeUser) { - return "STANDBY"; - } - if (SessionManager.activeUser.uid === 0) { - return "L0_ROOT"; - } - if (SessionManager.activeUser.uid === 1000) { - return "L5_ADMIN"; - } - return "OPERATOR"; - } + label: "FULL NAME" + value: SessionManager.activeUser ? SessionManager.activeUser.username.toUpperCase() : "UNKNOWN" } - } - InfoField { - id: employeeName - Layout.fillWidth: true - label: "FULL NAME" - value: SessionManager.activeUser ? SessionManager.activeUser.username.toUpperCase() : "UNKNOWN" + // Spacer + Item { + Layout.fillHeight: true + } + + // Row 3: Barcode — constrain height by aspect ratio of the SVG (241:72) + Image { + Layout.fillWidth: true + Layout.preferredHeight: Math.round(width * (72 / 241)) + fillMode: Image.Stretch + source: "../resources/id-barcode.svg" + } } + // ── Right column: profile picture ───────────────────────────────────── Item { + id: profilePicture + // Keep the photo panel square-ish: take 32% of total card width + Layout.preferredWidth: Math.round(root.width * 0.32) Layout.fillHeight: true - } - - Image { - Layout.bottomMargin: 2 // optical compensation - Layout.fillWidth: true - fillMode: Image.PreserveAspectFit - source: "../resources/id-barcode.svg" - } - } - - Item { - id: profilePicture - // width: parent.width * 0.40 - width: parent.height / 5 * 4 - height: parent.height - - anchors { - right: parent.right - } - Rectangle { - anchors.fill: parent - color: '#1effffff' - } + Rectangle { + anchors.fill: parent + color: "#1effffff" + } - Image { - source: "../resources/user.svg" - opacity: 0.9 + Image { + source: "../resources/user.svg" + opacity: 0.9 + anchors.fill: parent + fillMode: Image.PreserveAspectFit + anchors.margins: Math.round(4 * Units.vh) + } } } + // ========================================================================= + // Reveal animation + // ========================================================================= + SequentialAnimation { id: revealAnimation running: true @@ -125,7 +157,7 @@ Item { value: 0 } PropertyAction { - targets: [employeeId, employeeClass, employeeName] + targets: [employeeId, employeeClass, employeeAccess, employeeName] property: "fieldValueOpacity" value: 0 } @@ -157,6 +189,12 @@ Item { to: 1 duration: 150 } + NumberAnimation { + target: employeeAccess + property: "fieldValueOpacity" + to: 1 + duration: 150 + } SequentialAnimation { PauseAnimation { diff --git a/shell/greeter/components/MainLayout.qml b/shell/greeter/components/MainLayout.qml index 3ca2b62e..f720f2b3 100644 --- a/shell/greeter/components/MainLayout.qml +++ b/shell/greeter/components/MainLayout.qml @@ -345,6 +345,7 @@ Item { script: { fieldGroup.opacity = 0; accents.state = "identity_card"; + accents.tintColor = "#00ff41"; identityCard.start(); } } @@ -355,6 +356,7 @@ Item { script: { identityCard.opacity = 0; accents.state = "field_group"; + accents.tintColor = "transparent"; fieldGroup.opacity = 1; fieldGroup.start(); } diff --git a/shell/nix/home-manager.nix b/shell/nix/home-manager.nix index 9c859782..3149cef2 100644 --- a/shell/nix/home-manager.nix +++ b/shell/nix/home-manager.nix @@ -23,9 +23,10 @@ in }; Service = { + Type = "simple"; ExecStart = "${ctosPackage}/bin/ctos-shell"; Restart = "on-failure"; - RestartSec = 2; + RestartSec = 3; StartLimitBurst = 5; StartLimitIntervalSec = 30; }; diff --git a/shell/nix/package.nix b/shell/nix/package.nix index 590dda5a..ce806297 100644 --- a/shell/nix/package.nix +++ b/shell/nix/package.nix @@ -23,8 +23,13 @@ stdenvNoCC.mkDerivation { mkdir -p "$out/bin" cat << 'EOF' > "$out/bin/ctos-shell" #!/bin/sh - # Kill any existing instances to prevent duplicates - pkill -f "quickshell.*shell.qml" || true + LOCKFILE="''${XDG_RUNTIME_DIR:-/tmp}/ctos-shell.lock" + exec 9>"$LOCKFILE" + if ! flock -n 9; then + echo "ctos-shell: another instance is already running" >&2 + exit 0 + fi + echo $$ >&9 EOF cat << BIN >> "$out/bin/ctos-shell" exec ${pkgs.quickshell}/bin/quickshell -n -p "$out/share/ctos/shell.qml" "\$@"