From 7cdeba162531baecb2fd8dc302b4cc33b4a229a8 Mon Sep 17 00:00:00 2001 From: bbedward Date: Thu, 10 Jul 2025 14:14:16 -0400 Subject: [PATCH] Fix multi-monitor --- CLAUDE.md | 110 +++ shell-refactored.qml | 274 ------ shell-test.qml | 269 ----- shell-working.qml | 2246 ------------------------------------------ shell.qml | 112 ++- 5 files changed, 195 insertions(+), 2816 deletions(-) create mode 100644 CLAUDE.md delete mode 100644 shell-refactored.qml delete mode 100644 shell-test.qml delete mode 100644 shell-working.qml diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..7c5a0d68 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,110 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a Quickshell-based desktop shell implementation with Material Design 3 dark theme. The shell provides a complete desktop environment experience with panels, widgets, and system integration services. + +## Technology Stack + +- **QML (Qt Modeling Language)** - Primary language for all UI components +- **Quickshell Framework** - QML-based framework for building desktop shells +- **Qt/QtQuick** - UI rendering and controls +- **Qt5Compat** - Graphical effects +- **Wayland** - Display server protocol + +## Development Commands + +Since this is a Quickshell-based project without traditional build configuration files, development typically involves: + +```bash +# Run the shell (requires Quickshell to be installed) +quickshell -p shell.qml + +# Or use the shorthand +qs -p . +``` + +## Architecture Overview + +### Component Organization + +1. **Shell Entry Point** (root directory) + - `shell.qml` - Main shell implementation with multi-monitor support + +2. **Widgets/** - Reusable UI components + - Each widget is a self-contained QML module with its own `qmldir` + - Examples: TopBar, ClockWidget, SystemTrayWidget, NotificationWidget + - Components follow Material Design 3 principles + +3. **Services/** - Backend services and controllers + - `MprisController.qml` - Media player integration + - `OSDetectionService.qml` - Operating system detection + - `WeatherService.qml` - Weather data fetching + - Services handle system integration and data management + +### Key Architectural Patterns + +1. **Module System**: Each component directory contains a `qmldir` file defining the module exports +2. **Property Bindings**: Heavy use of Qt property bindings for reactive UI updates +3. **Singleton Services**: Services are typically instantiated once and shared across components +4. **Material Design Theming**: Consistent use of Material Design 3 color properties throughout + +### Important Components + +- **ControlCenter**: Central hub for system controls (WiFi, Bluetooth, brightness, volume) +- **ApplicationLauncher**: App grid and search functionality +- **NotificationSystem**: Notification display and management +- **ClipboardHistory**: Clipboard manager with history +- **WorkspaceSwitcher**: Per-display virtual desktop switching with Niri integration + +## Code Conventions + +1. **QML Style**: + - Use 4-space indentation + - Properties before signal handlers + - ID should be the first property + - Prefer property bindings over imperative code + +2. **Component Structure**: + ```qml + Item { + id: root + + // Properties + property type name: value + + // Signal handlers + onSignal: { } + + // Child components + Component { } + } + ``` + +3. **Service Integration**: Components should communicate with services through properties and signals rather than direct method calls + +## Multi-Monitor Support + +The shell uses Quickshell's `Variants` pattern for multi-monitor support: +- Each connected monitor gets its own top bar instance +- Workspace switchers are per-display and Niri-aware +- Monitors are automatically detected by screen name (DP-1, DP-2, etc.) +- Workspaces are dynamically synchronized with Niri's per-output workspaces + +## Common Tasks + +When modifying the shell: +1. Test changes with `qs -p .` +2. Check that animations remain smooth (60 FPS target) +3. Ensure Material Design 3 color consistency +4. Test on Wayland session +5. Verify multi-monitor behavior if applicable + +When adding new widgets: +1. Create directory under `Widgets/` +2. Add `qmldir` file with module definition +3. Follow existing widget patterns for property exposure +4. Integrate with relevant services as needed +5. Consider whether the widget should be per-screen or global \ No newline at end of file diff --git a/shell-refactored.qml b/shell-refactored.qml deleted file mode 100644 index 81140880..00000000 --- a/shell-refactored.qml +++ /dev/null @@ -1,274 +0,0 @@ -//@ pragma UseQApplication - -import QtQuick -import QtQuick.Controls -import Qt5Compat.GraphicalEffects -import Quickshell -import Quickshell.Widgets -import Quickshell.Wayland -import Quickshell.Io -import Quickshell.Services.SystemTray -import Quickshell.Services.Notifications -import Quickshell.Services.Mpris -import "Services" -import "Widgets" - -ShellRoot { - id: root - - property bool calendarVisible: false - property bool showTrayMenu: false - property real trayMenuX: 0 - property real trayMenuY: 0 - property var currentTrayMenu: null - property var currentTrayItem: null - property bool notificationHistoryVisible: false - property var activeNotification: null - property bool showNotificationPopup: false - property bool mediaPlayerVisible: false - property MprisPlayer activePlayer: MprisController.activePlayer - property bool hasActiveMedia: MprisController.isPlaying && (activePlayer?.trackTitle || activePlayer?.trackArtist) - - property bool useFahrenheit: true - property var weather: WeatherService.weather - property string osLogo: OSDetectionService.osLogo - property string osName: OSDetectionService.osName - - property var notificationHistory: notificationHistoryModel - property var appLauncher: appLauncherPopup - property var clipboardHistoryPopup: clipboardHistoryPopupInstance - property var colorPickerProcess: colorPickerProcessInstance - - property var weatherIcons: ({ - "113": "clear_day", - "116": "partly_cloudy_day", - "119": "cloud", - "122": "cloud", - "143": "foggy", - "176": "rainy", - "179": "rainy", - "182": "rainy", - "185": "rainy", - "200": "thunderstorm", - "227": "cloudy_snowing", - "230": "snowing_heavy", - "248": "foggy", - "260": "foggy", - "263": "rainy", - "266": "rainy", - "281": "rainy", - "284": "rainy", - "293": "rainy", - "296": "rainy", - "299": "rainy", - "302": "weather_hail", - "305": "rainy", - "308": "weather_hail", - "311": "rainy", - "314": "rainy", - "317": "rainy", - "320": "cloudy_snowing", - "323": "cloudy_snowing", - "326": "cloudy_snowing", - "329": "snowing_heavy", - "332": "snowing_heavy", - "335": "snowing", - "338": "snowing_heavy", - "350": "rainy", - "353": "rainy", - "356": "rainy", - "359": "weather_hail", - "362": "rainy", - "365": "rainy", - "368": "cloudy_snowing", - "371": "snowing", - "374": "rainy", - "377": "rainy", - "386": "thunderstorm", - "389": "thunderstorm", - "392": "thunderstorm", - "395": "snowing" - }) - - QtObject { - id: theme - - property color primary: "#D0BCFF" - property color primaryText: "#381E72" - property color primaryContainer: "#4F378B" - property color secondary: "#CCC2DC" - property color surface: "#10121E" - property color surfaceText: "#E6E0E9" - property color surfaceVariant: "#49454F" - property color surfaceVariantText: "#CAC4D0" - property color surfaceTint: "#D0BCFF" - property color background: "#10121E" - property color backgroundText: "#E6E0E9" - property color outline: "#938F99" - property color surfaceContainer: "#1D1B20" - property color surfaceContainerHigh: "#2B2930" - property color archBlue: "#1793D1" - property color success: "#4CAF50" - property color warning: "#FF9800" - property color info: "#2196F3" - property color error: "#F2B8B5" - - property int shortDuration: 150 - property int mediumDuration: 300 - property int longDuration: 500 - property int extraLongDuration: 1000 - - property int standardEasing: Easing.OutCubic - property int emphasizedEasing: Easing.OutQuart - - property real cornerRadius: 12 - property real cornerRadiusSmall: 8 - property real cornerRadiusLarge: 16 - property real cornerRadiusXLarge: 24 - - property real spacingXS: 4 - property real spacingS: 8 - property real spacingM: 12 - property real spacingL: 16 - property real spacingXL: 24 - - property real fontSizeSmall: 12 - property real fontSizeMedium: 14 - property real fontSizeLarge: 16 - property real fontSizeXLarge: 20 - - property real barHeight: 48 - property real iconSize: 24 - property real iconSizeSmall: 16 - property real iconSizeLarge: 32 - - property real opacityDisabled: 0.38 - property real opacityMedium: 0.60 - property real opacityHigh: 0.87 - property real opacityFull: 1.0 - - property string iconFont: "Material Symbols Rounded" - property string iconFontFilled: "Material Symbols Rounded" - property int iconFontWeight: Font.Normal - property int iconFontFilledWeight: Font.Medium - } - - TopBar { - id: topBar - theme: root.theme - root: root - } - - AppLauncher { - id: appLauncherPopup - theme: root.theme - } - - ClipboardHistory { - id: clipboardHistoryPopupInstance - theme: root.theme - } - - MediaPlayer { - id: mediaPlayer - theme: root.theme - isVisible: root.mediaPlayerVisible - } - - Process { - id: colorPickerProcessInstance - command: ["hyprpicker", "-a"] - running: false - - onExited: (exitCode) => { - if (exitCode !== 0) { - console.warn("Color picker failed. Make sure hyprpicker is installed: yay -S hyprpicker") - } - } - } - - NotificationServer { - id: notificationServer - actionsSupported: true - bodyMarkupSupported: true - imageSupported: true - keepOnReload: false - persistenceSupported: true - - onNotification: (notification) => { - if (!notification || !notification.id) return - - if (!notification.appName && !notification.summary && !notification.body) { - return - } - - console.log("New notification from:", notification.appName || "Unknown", "Summary:", notification.summary || "No summary") - - var notifObj = { - "id": notification.id, - "appName": notification.appName || "App", - "summary": notification.summary || "", - "body": notification.body || "", - "timestamp": new Date(), - "appIcon": notification.appIcon || notification.icon || "", - "icon": notification.icon || "", - "image": notification.image || "" - } - - notificationHistoryModel.insert(0, notifObj) - - while (notificationHistoryModel.count > 50) { - notificationHistoryModel.remove(notificationHistoryModel.count - 1) - } - - root.activeNotification = notifObj - root.showNotificationPopup = true - notificationTimer.restart() - } - } - - ListModel { - id: notificationHistoryModel - } - - Timer { - id: notificationTimer - interval: 5000 - repeat: false - onTriggered: hideNotificationPopup() - } - - Timer { - id: clearNotificationTimer - interval: theme.mediumDuration + 50 - repeat: false - onTriggered: root.activeNotification = null - } - - function showNotificationPopup(notification) { - root.activeNotification = notification - root.showNotificationPopup = true - notificationTimer.restart() - } - - function hideNotificationPopup() { - root.showNotificationPopup = false - notificationTimer.stop() - clearNotificationTimer.restart() - } - - Timer { - running: root.activePlayer?.playbackState === MprisPlaybackState.Playing - interval: 1000 - repeat: true - onTriggered: { - if (root.activePlayer) { - root.activePlayer.positionChanged() - } - } - } - - Component.onCompleted: { - console.log("DankMaterialDark shell loaded successfully!") - } -} \ No newline at end of file diff --git a/shell-test.qml b/shell-test.qml deleted file mode 100644 index 4a62b188..00000000 --- a/shell-test.qml +++ /dev/null @@ -1,269 +0,0 @@ -//@ pragma UseQApplication - -import QtQuick -import QtQuick.Controls -import Qt5Compat.GraphicalEffects -import Quickshell -import Quickshell.Widgets -import Quickshell.Wayland -import Quickshell.Io -import Quickshell.Services.SystemTray -import Quickshell.Services.Notifications -import Quickshell.Services.Mpris -import "Services" -import "Widgets" - -ShellRoot { - id: root - - property bool calendarVisible: false - property bool showTrayMenu: false - property real trayMenuX: 0 - property real trayMenuY: 0 - property var currentTrayMenu: null - property var currentTrayItem: null - property bool notificationHistoryVisible: false - property var activeNotification: null - property bool showNotificationPopup: false - property bool mediaPlayerVisible: false - property MprisPlayer activePlayer: MprisController.activePlayer - property bool hasActiveMedia: MprisController.isPlaying && (activePlayer?.trackTitle || activePlayer?.trackArtist) - - property bool useFahrenheit: true - property var weather: WeatherService.weather - property string osLogo: OSDetectionService.osLogo - property string osName: OSDetectionService.osName - - property var notificationHistory: notificationHistoryModel - property var appLauncher: appLauncherPopup - property var clipboardHistoryPopup: clipboardHistoryPopupInstance - property var colorPickerProcess: colorPickerProcessInstance - - property var weatherIcons: ({ - "113": "clear_day", - "116": "partly_cloudy_day", - "119": "cloud", - "122": "cloud", - "143": "foggy", - "176": "rainy", - "179": "rainy", - "182": "rainy", - "185": "rainy", - "200": "thunderstorm", - "227": "cloudy_snowing", - "230": "snowing_heavy", - "248": "foggy", - "260": "foggy", - "263": "rainy", - "266": "rainy", - "281": "rainy", - "284": "rainy", - "293": "rainy", - "296": "rainy", - "299": "rainy", - "302": "weather_hail", - "305": "rainy", - "308": "weather_hail", - "311": "rainy", - "314": "rainy", - "317": "rainy", - "320": "cloudy_snowing", - "323": "cloudy_snowing", - "326": "cloudy_snowing", - "329": "snowing_heavy", - "332": "snowing_heavy", - "335": "snowing", - "338": "snowing_heavy", - "350": "rainy", - "353": "rainy", - "356": "rainy", - "359": "weather_hail", - "362": "rainy", - "365": "rainy", - "368": "cloudy_snowing", - "371": "snowing", - "374": "rainy", - "377": "rainy", - "386": "thunderstorm", - "389": "thunderstorm", - "392": "thunderstorm", - "395": "snowing" - }) - - QtObject { - id: theme - - property color primary: "#D0BCFF" - property color primaryText: "#381E72" - property color primaryContainer: "#4F378B" - property color secondary: "#CCC2DC" - property color surface: "#10121E" - property color surfaceText: "#E6E0E9" - property color surfaceVariant: "#49454F" - property color surfaceVariantText: "#CAC4D0" - property color surfaceTint: "#D0BCFF" - property color background: "#10121E" - property color backgroundText: "#E6E0E9" - property color outline: "#938F99" - property color surfaceContainer: "#1D1B20" - property color surfaceContainerHigh: "#2B2930" - property color archBlue: "#1793D1" - property color success: "#4CAF50" - property color warning: "#FF9800" - property color info: "#2196F3" - property color error: "#F2B8B5" - - property int shortDuration: 150 - property int mediumDuration: 300 - property int longDuration: 500 - property int extraLongDuration: 1000 - - property int standardEasing: Easing.OutCubic - property int emphasizedEasing: Easing.OutQuart - - property real cornerRadius: 12 - property real cornerRadiusSmall: 8 - property real cornerRadiusLarge: 16 - property real cornerRadiusXLarge: 24 - - property real spacingXS: 4 - property real spacingS: 8 - property real spacingM: 12 - property real spacingL: 16 - property real spacingXL: 24 - - property real fontSizeSmall: 12 - property real fontSizeMedium: 14 - property real fontSizeLarge: 16 - property real fontSizeXLarge: 20 - - property real barHeight: 48 - property real iconSize: 24 - property real iconSizeSmall: 16 - property real iconSizeLarge: 32 - - property real opacityDisabled: 0.38 - property real opacityMedium: 0.60 - property real opacityHigh: 0.87 - property real opacityFull: 1.0 - - property string iconFont: "Material Symbols Rounded" - property string iconFontFilled: "Material Symbols Rounded" - property int iconFontWeight: Font.Normal - property int iconFontFilledWeight: Font.Medium - } - - TopBarSimple { - id: topBar - theme: root.theme - root: root - } - - AppLauncher { - id: appLauncherPopup - theme: root.theme - } - - ClipboardHistory { - id: clipboardHistoryPopupInstance - theme: root.theme - } - - - Process { - id: colorPickerProcessInstance - command: ["hyprpicker", "-a"] - running: false - - onExited: (exitCode) => { - if (exitCode !== 0) { - console.warn("Color picker failed. Make sure hyprpicker is installed: yay -S hyprpicker") - } - } - } - - NotificationServer { - id: notificationServer - actionsSupported: true - bodyMarkupSupported: true - imageSupported: true - keepOnReload: false - persistenceSupported: true - - onNotification: (notification) => { - if (!notification || !notification.id) return - - if (!notification.appName && !notification.summary && !notification.body) { - return - } - - console.log("New notification from:", notification.appName || "Unknown", "Summary:", notification.summary || "No summary") - - var notifObj = { - "id": notification.id, - "appName": notification.appName || "App", - "summary": notification.summary || "", - "body": notification.body || "", - "timestamp": new Date(), - "appIcon": notification.appIcon || notification.icon || "", - "icon": notification.icon || "", - "image": notification.image || "" - } - - notificationHistoryModel.insert(0, notifObj) - - while (notificationHistoryModel.count > 50) { - notificationHistoryModel.remove(notificationHistoryModel.count - 1) - } - - root.activeNotification = notifObj - root.showNotificationPopup = true - notificationTimer.restart() - } - } - - ListModel { - id: notificationHistoryModel - } - - Timer { - id: notificationTimer - interval: 5000 - repeat: false - onTriggered: hideNotificationPopup() - } - - Timer { - id: clearNotificationTimer - interval: theme.mediumDuration + 50 - repeat: false - onTriggered: root.activeNotification = null - } - - function showNotificationPopup(notification) { - root.activeNotification = notification - root.showNotificationPopup = true - notificationTimer.restart() - } - - function hideNotificationPopup() { - root.showNotificationPopup = false - notificationTimer.stop() - clearNotificationTimer.restart() - } - - Timer { - running: root.activePlayer?.playbackState === MprisPlaybackState.Playing - interval: 1000 - repeat: true - onTriggered: { - if (root.activePlayer) { - root.activePlayer.positionChanged() - } - } - } - - Component.onCompleted: { - console.log("DankMaterialDark shell loaded successfully!") - } -} \ No newline at end of file diff --git a/shell-working.qml b/shell-working.qml deleted file mode 100644 index ae60527f..00000000 --- a/shell-working.qml +++ /dev/null @@ -1,2246 +0,0 @@ -//@ pragma UseQApplication - -import QtQuick -import QtQuick.Controls -import Qt5Compat.GraphicalEffects -import Quickshell -import Quickshell.Widgets -import Quickshell.Wayland -import Quickshell.Io -import Quickshell.Services.SystemTray -import Quickshell.Services.Notifications -import Quickshell.Services.Mpris -import "Services" - -ShellRoot { - id: root - - property bool calendarVisible: false - property bool showTrayMenu: false - property real trayMenuX: 0 - property real trayMenuY: 0 - property var currentTrayMenu: null - property var currentTrayItem: null - property string osLogo: "" - property string osName: "" - property bool notificationHistoryVisible: false - property var activeNotification: null - property bool showNotificationPopup: false - property bool mediaPlayerVisible: false - property MprisPlayer activePlayer: MprisController.activePlayer - property bool hasActiveMedia: MprisController.isPlaying && (activePlayer?.trackTitle || activePlayer?.trackArtist) - - // Weather data - property var weather: ({ - available: false, - temp: 0, - tempF: 0, - city: "", - wCode: "113", - humidity: 0, - wind: "", - sunrise: "06:00", - sunset: "18:00", - uv: 0, - pressure: 0 - }) - - // Weather configuration - property bool useFahrenheit: true // Default to Fahrenheit - - // Material 3 theme system - QtObject { - id: theme - - property color primary: "#D0BCFF" - property color primaryText: "#381E72" - property color primaryContainer: "#4F378B" - property color secondary: "#CCC2DC" - property color surface: "#10121E" - property color surfaceText: "#E6E0E9" - property color surfaceVariant: "#49454F" - property color surfaceVariantText: "#CAC4D0" - property color surfaceTint: "#D0BCFF" - property color background: "#10121E" - property color backgroundText: "#E6E0E9" - property color outline: "#938F99" - property color surfaceContainer: "#1D1B20" - property color surfaceContainerHigh: "#2B2930" - property color archBlue: "#1793D1" - property color success: "#4CAF50" - property color warning: "#FF9800" - property color info: "#2196F3" - property color error: "#F2B8B5" - - property int shortDuration: 150 - property int mediumDuration: 300 - property int longDuration: 500 - property int extraLongDuration: 1000 - - property int standardEasing: Easing.OutCubic - property int emphasizedEasing: Easing.OutQuart - - property real cornerRadius: 12 - property real cornerRadiusSmall: 8 - property real cornerRadiusLarge: 16 - property real cornerRadiusXLarge: 24 - - property real spacingXS: 4 - property real spacingS: 8 - property real spacingM: 12 - property real spacingL: 16 - property real spacingXL: 24 - - property real fontSizeSmall: 12 - property real fontSizeMedium: 14 - property real fontSizeLarge: 16 - property real fontSizeXLarge: 20 - - property real barHeight: 48 - property real iconSize: 24 - property real iconSizeSmall: 16 - property real iconSizeLarge: 32 - - property real opacityDisabled: 0.38 - property real opacityMedium: 0.60 - property real opacityHigh: 0.87 - property real opacityFull: 1.0 - - property string iconFont: "Material Symbols Rounded" - property string iconFontFilled: "Material Symbols Rounded" - property int iconFontWeight: Font.Normal - property int iconFontFilledWeight: Font.Medium - } - - // Weather icon mapping (based on wttr.in weather codes) - property var weatherIcons: ({ - "113": "clear_day", - "116": "partly_cloudy_day", - "119": "cloud", - "122": "cloud", - "143": "foggy", - "176": "rainy", - "179": "rainy", - "182": "rainy", - "185": "rainy", - "200": "thunderstorm", - "227": "cloudy_snowing", - "230": "snowing_heavy", - "248": "foggy", - "260": "foggy", - "263": "rainy", - "266": "rainy", - "281": "rainy", - "284": "rainy", - "293": "rainy", - "296": "rainy", - "299": "rainy", - "302": "weather_hail", - "305": "rainy", - "308": "weather_hail", - "311": "rainy", - "314": "rainy", - "317": "rainy", - "320": "cloudy_snowing", - "323": "cloudy_snowing", - "326": "cloudy_snowing", - "329": "snowing_heavy", - "332": "snowing_heavy", - "335": "snowing", - "338": "snowing_heavy", - "350": "rainy", - "353": "rainy", - "356": "rainy", - "359": "weather_hail", - "362": "rainy", - "365": "rainy", - "368": "cloudy_snowing", - "371": "snowing", - "374": "rainy", - "377": "rainy", - "386": "thunderstorm", - "389": "thunderstorm", - "392": "thunderstorm", - "395": "snowing" - }) - - // Top bar - PanelWindow { - id: topBar - - anchors { - top: true - left: true - right: true - } - - implicitHeight: theme.barHeight - color: "transparent" - - Rectangle { - anchors.fill: parent - color: Qt.rgba(theme.surfaceContainer.r, theme.surfaceContainer.g, theme.surfaceContainer.b, 0.95) - - Rectangle { - anchors.fill: parent - color: "transparent" - border.color: Qt.rgba(theme.outline.r, theme.outline.g, theme.outline.b, 0.12) - border.width: 1 - } - - Rectangle { - anchors.fill: parent - color: Qt.rgba(theme.surfaceTint.r, theme.surfaceTint.g, theme.surfaceTint.b, 0.08) - - SequentialAnimation on opacity { - running: true - loops: Animation.Infinite - NumberAnimation { - to: 0.12 - duration: theme.extraLongDuration - easing.type: theme.standardEasing - } - NumberAnimation { - to: 0.06 - duration: theme.extraLongDuration - easing.type: theme.standardEasing - } - } - } - } - - Item { - anchors.fill: parent - anchors.leftMargin: theme.spacingL - anchors.rightMargin: theme.spacingL - - Row { - id: leftSection - height: parent.height - spacing: theme.spacingL - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - - Rectangle { - id: archLauncher - width: Math.max(120, launcherRow.implicitWidth + theme.spacingM * 2) - height: 32 - radius: theme.cornerRadius - color: launcherArea.containsMouse ? Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.12) : Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.08) - anchors.verticalCenter: parent.verticalCenter - - Row { - id: launcherRow - anchors.centerIn: parent - spacing: theme.spacingS - - Text { - anchors.verticalCenter: parent.verticalCenter - text: root.osLogo || "apps" // Use OS logo if detected, fallback to apps icon - font.family: root.osLogo ? "NerdFont" : theme.iconFont - font.pixelSize: root.osLogo ? theme.iconSize - 2 : theme.iconSize - 2 - font.weight: theme.iconFontWeight - color: theme.surfaceText - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - - Text { - anchors.verticalCenter: parent.verticalCenter - text: "Applications" - font.pixelSize: theme.fontSizeMedium - font.weight: Font.Medium - color: theme.surfaceText - } - } - - MouseArea { - id: launcherArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: { - appLauncher.toggle() - } - } - - Behavior on color { - ColorAnimation { - duration: theme.shortDuration - easing.type: theme.standardEasing - } - } - } - - Rectangle { - id: workspaceSwitcher - width: Math.max(120, workspaceRow.implicitWidth + theme.spacingL * 2) - height: 32 - radius: theme.cornerRadiusLarge - color: Qt.rgba(theme.surfaceContainerHigh.r, theme.surfaceContainerHigh.g, theme.surfaceContainerHigh.b, 0.8) - anchors.verticalCenter: parent.verticalCenter - - property int currentWorkspace: 1 - property var workspaceList: [] - - Process { - id: workspaceQuery - command: ["niri", "msg", "workspaces"] - running: true - - stdout: SplitParser { - splitMarker: "\n" - onRead: (data) => { - if (data.trim()) { - workspaceSwitcher.parseWorkspaceOutput(data.trim()) - } - } - } - } - - function parseWorkspaceOutput(data) { - const lines = data.split('\n') - let currentOutputName = "" - let focusedOutput = "" - let focusedWorkspace = 1 - let outputWorkspaces = {} - - for (const line of lines) { - if (line.startsWith('Output "')) { - const outputMatch = line.match(/Output "(.+)"/) - if (outputMatch) { - currentOutputName = outputMatch[1] - outputWorkspaces[currentOutputName] = [] - } - continue - } - - if (line.trim() && line.match(/^\s*\*?\s*(\d+)$/)) { - const wsMatch = line.match(/^\s*(\*?)\s*(\d+)$/) - if (wsMatch) { - const isActive = wsMatch[1] === '*' - const wsNum = parseInt(wsMatch[2]) - - if (currentOutputName && outputWorkspaces[currentOutputName]) { - outputWorkspaces[currentOutputName].push(wsNum) - } - - if (isActive) { - focusedOutput = currentOutputName - focusedWorkspace = wsNum - } - } - } - } - - currentWorkspace = focusedWorkspace - - if (focusedOutput && outputWorkspaces[focusedOutput]) { - workspaceList = outputWorkspaces[focusedOutput] - } else { - workspaceList = [1, 2] - } - } - - Timer { - interval: 500 - running: true - repeat: true - onTriggered: { - workspaceQuery.running = true - } - } - - Row { - id: workspaceRow - anchors.centerIn: parent - spacing: theme.spacingS - - Repeater { - model: workspaceSwitcher.workspaceList - - Rectangle { - property bool isActive: modelData === workspaceSwitcher.currentWorkspace - property bool isHovered: mouseArea.containsMouse - - width: isActive ? theme.spacingXL + theme.spacingS : theme.spacingL - height: theme.spacingS - radius: height / 2 - color: isActive ? theme.primary : - isHovered ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.5) : - Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.3) - - Behavior on width { - NumberAnimation { - duration: theme.mediumDuration - easing.type: theme.emphasizedEasing - } - } - - Behavior on color { - ColorAnimation { - duration: theme.mediumDuration - easing.type: theme.emphasizedEasing - } - } - - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: { - switchProcess.command = ["niri", "msg", "action", "focus-workspace", modelData.toString()] - switchProcess.running = true - workspaceSwitcher.currentWorkspace = modelData - Qt.callLater(() => { - workspaceQuery.running = true - }) - } - } - } - } - } - - Process { - id: switchProcess - running: false - } - } - } - - Rectangle { - id: clockContainer - width: Math.min(root.hasActiveMedia ? 500 : (root.weather.available ? 280 : 200), parent.width - theme.spacingL * 2) - height: root.hasActiveMedia ? 80 : 32 - radius: theme.cornerRadius - color: clockMouseArea.containsMouse && root.hasActiveMedia ? - Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) : - Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.08) - anchors.centerIn: parent - - Behavior on color { - ColorAnimation { - duration: theme.shortDuration - easing.type: theme.standardEasing - } - } - - property date currentDate: new Date() - - // Media player content (when active) - Column { - visible: root.hasActiveMedia - anchors.centerIn: parent - width: parent.width - theme.spacingM * 2 - spacing: theme.spacingXS - - Row { - width: parent.width - spacing: theme.spacingS - - Rectangle { - width: 48 - height: 48 - radius: theme.cornerRadiusSmall - color: Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.3) - - Item { - anchors.fill: parent - clip: true - - Image { - anchors.fill: parent - source: root.activePlayer?.trackArtUrl || "" - fillMode: Image.PreserveAspectCrop - smooth: true - } - - Rectangle { - anchors.fill: parent - visible: parent.children[0].status !== Image.Ready - color: "transparent" - - Text { - anchors.centerIn: parent - text: "music_note" - font.family: theme.iconFont - font.pixelSize: theme.iconSize - color: theme.surfaceVariantText - } - } - } - } - - Column { - width: parent.width - 48 - theme.spacingS - 120 - spacing: 2 - anchors.verticalCenter: parent.verticalCenter - - Text { - text: root.activePlayer?.trackTitle || "Unknown Track" - font.pixelSize: theme.fontSizeMedium - color: theme.surfaceText - font.weight: Font.Medium - width: parent.width - elide: Text.ElideRight - } - - Text { - text: root.activePlayer?.trackArtist || "Unknown Artist" - font.pixelSize: theme.fontSizeSmall - color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.7) - width: parent.width - elide: Text.ElideRight - } - } - - Row { - anchors.verticalCenter: parent.verticalCenter - spacing: theme.spacingS - - Rectangle { - width: 28 - height: 28 - radius: 14 - color: prevBtnArea.containsMouse ? Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.12) : "transparent" - - Text { - anchors.centerIn: parent - text: "skip_previous" - font.family: theme.iconFont - font.pixelSize: 16 - color: theme.surfaceText - } - - MouseArea { - id: prevBtnArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: root.activePlayer?.previous() - } - } - - Rectangle { - width: 28 - height: 28 - radius: 14 - color: theme.primary - - Text { - anchors.centerIn: parent - text: root.activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow" - font.family: theme.iconFont - font.pixelSize: 16 - color: theme.background - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: root.activePlayer?.togglePlaying() - } - } - - Rectangle { - width: 28 - height: 28 - radius: 14 - color: nextBtnArea.containsMouse ? Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.12) : "transparent" - - Text { - anchors.centerIn: parent - text: "skip_next" - font.family: theme.iconFont - font.pixelSize: 16 - color: theme.surfaceText - } - - MouseArea { - id: nextBtnArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: root.activePlayer?.next() - } - } - } - } - - Rectangle { - width: parent.width - height: 4 - radius: 2 - color: Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.3) - - Rectangle { - id: progressFill - width: parent.width * (root.activePlayer?.position / Math.max(root.activePlayer?.length || 1, 1)) - height: parent.height - radius: parent.radius - color: theme.primary - - Behavior on width { - NumberAnimation { - duration: 200 - easing.type: Easing.OutQuad - } - } - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - - onClicked: (mouse) => { - if (root.activePlayer && root.activePlayer.length > 0) { - const newPosition = (mouse.x / width) * root.activePlayer.length - root.activePlayer.setPosition(newPosition) - } - } - } - } - } - - // Normal clock/weather content (when no media) - Row { - anchors.centerIn: parent - spacing: theme.spacingM - visible: !root.hasActiveMedia - - // Weather info (when available) - Row { - spacing: theme.spacingXS - visible: root.weather.available - anchors.verticalCenter: parent.verticalCenter - - Text { - text: root.weatherIcons[root.weather.wCode] || "clear_day" - font.family: theme.iconFont - font.pixelSize: theme.iconSize - 2 - color: theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - Text { - text: (root.useFahrenheit ? root.weather.tempF : root.weather.temp) + "°" + (root.useFahrenheit ? "F" : "C") - font.pixelSize: theme.fontSizeMedium - color: theme.surfaceText - font.weight: Font.Medium - anchors.verticalCenter: parent.verticalCenter - } - } - - // Separator when weather is available - Text { - text: "•" - font.pixelSize: theme.fontSizeMedium - color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.5) - anchors.verticalCenter: parent.verticalCenter - visible: root.weather.available - } - - // Time and date - Row { - spacing: theme.spacingS - anchors.verticalCenter: parent.verticalCenter - - Text { - text: Qt.formatTime(clockContainer.currentDate, "h:mm AP") - font.pixelSize: theme.fontSizeMedium - color: theme.surfaceText - font.weight: Font.Medium - anchors.verticalCenter: parent.verticalCenter - } - - Text { - text: "•" - font.pixelSize: theme.fontSizeMedium - color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.5) - anchors.verticalCenter: parent.verticalCenter - } - - Text { - text: Qt.formatDate(clockContainer.currentDate, "ddd d") - font.pixelSize: theme.fontSizeMedium - color: theme.surfaceText - font.weight: Font.Medium - anchors.verticalCenter: parent.verticalCenter - } - } - } - - Timer { - interval: 1000 - running: true - repeat: true - onTriggered: { - clockContainer.currentDate = new Date() - } - } - - MouseArea { - id: clockMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: !root.hasActiveMedia ? Qt.PointingHandCursor : Qt.ArrowCursor - enabled: !root.hasActiveMedia - - onClicked: { - root.calendarVisible = !root.calendarVisible - } - } - } - - Row { - id: rightSection - height: parent.height - spacing: theme.spacingXS - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - - Rectangle { - width: Math.max(40, systemTrayRow.implicitWidth + theme.spacingS * 2) - height: 32 - radius: theme.cornerRadius - color: Qt.rgba(theme.secondary.r, theme.secondary.g, theme.secondary.b, 0.08) - anchors.verticalCenter: parent.verticalCenter - visible: systemTrayRow.children.length > 0 - - Row { - id: systemTrayRow - anchors.centerIn: parent - spacing: theme.spacingXS - - Repeater { - model: SystemTray.items - delegate: Rectangle { - width: 24 - height: 24 - radius: theme.cornerRadiusSmall - color: trayItemArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) : "transparent" - - property var trayItem: modelData - - Image { - anchors.centerIn: parent - width: 18 - height: 18 - source: { - let icon = trayItem?.icon || ""; - if (!icon) return ""; - - if (icon.includes("?path=")) { - const [name, path] = icon.split("?path="); - const fileName = name.substring(name.lastIndexOf("/") + 1); - return `file://${path}/${fileName}`; - } - return icon; - } - asynchronous: true - smooth: true - fillMode: Image.PreserveAspectFit - } - - MouseArea { - id: trayItemArea - anchors.fill: parent - acceptedButtons: Qt.LeftButton | Qt.RightButton - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: (mouse) => { - if (!trayItem) return; - - if (mouse.button === Qt.LeftButton) { - if (!trayItem.onlyMenu) { - trayItem.activate() - } - } else if (mouse.button === Qt.RightButton) { - if (trayItem.hasMenu) { - console.log("Right-click detected, showing menu for:", trayItem.title || "Unknown") - customTrayMenu.showMenu(mouse.x, mouse.y) - } else { - console.log("No menu available for:", trayItem.title || "Unknown") - } - } - } - } - - // Custom Material 3 styled menu - QtObject { - id: customTrayMenu - - property bool menuVisible: false - - function showMenu(x, y) { - root.currentTrayMenu = customTrayMenu - root.currentTrayItem = trayItem - - // Simple positioning: right side of screen, below the panel - root.trayMenuX = rightSection.x + rightSection.width - 180 - theme.spacingL - root.trayMenuY = theme.barHeight + theme.spacingS - - console.log("Showing menu at:", root.trayMenuX, root.trayMenuY) - menuVisible = true - root.showTrayMenu = true - } - - function hideMenu() { - menuVisible = false - root.showTrayMenu = false - root.currentTrayMenu = null - root.currentTrayItem = null - } - } - - Behavior on color { - ColorAnimation { - duration: theme.shortDuration - easing.type: theme.standardEasing - } - } - } - } - } - } - - // Clipboard History Button - Rectangle { - width: 40 - height: 32 - radius: theme.cornerRadius - color: clipboardArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) : Qt.rgba(theme.secondary.r, theme.secondary.g, theme.secondary.b, 0.08) - anchors.verticalCenter: parent.verticalCenter - - Text { - anchors.centerIn: parent - text: "content_paste" // Material icon for clipboard - font.family: theme.iconFont - font.pixelSize: theme.iconSize - 6 - font.weight: theme.iconFontWeight - color: theme.surfaceText - } - - MouseArea { - id: clipboardArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: { - clipboardHistoryPopup.toggle() - } - } - - Behavior on color { - ColorAnimation { - duration: theme.shortDuration - easing.type: theme.standardEasing - } - } - } - - // Color Picker Button - Rectangle { - width: 40 - height: 32 - radius: theme.cornerRadius - color: colorPickerArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) : Qt.rgba(theme.secondary.r, theme.secondary.g, theme.secondary.b, 0.08) - anchors.verticalCenter: parent.verticalCenter - - Text { - anchors.centerIn: parent - text: "colorize" // Material icon for color picker - font.family: theme.iconFont - font.pixelSize: theme.iconSize - 6 - font.weight: theme.iconFontWeight - color: theme.surfaceText - } - - MouseArea { - id: colorPickerArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: { - colorPickerProcess.running = true - } - } - - Behavior on color { - ColorAnimation { - duration: theme.shortDuration - easing.type: theme.standardEasing - } - } - } - - // Notification Center Button - Rectangle { - width: 40 - height: 32 - radius: theme.cornerRadius - color: notificationArea.containsMouse || root.notificationHistoryVisible ? - Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.16) : - Qt.rgba(theme.secondary.r, theme.secondary.g, theme.secondary.b, 0.08) - anchors.verticalCenter: parent.verticalCenter - - property bool hasUnread: notificationHistory.count > 0 - - Text { - anchors.centerIn: parent - text: "notifications" // Material icon for notifications - font.family: theme.iconFont - font.pixelSize: theme.iconSize - 6 - font.weight: theme.iconFontWeight - color: notificationArea.containsMouse || root.notificationHistoryVisible ? - theme.primary : theme.surfaceText - } - - // Notification dot indicator - Rectangle { - width: 8 - height: 8 - radius: 4 - color: theme.error - anchors.right: parent.right - anchors.top: parent.top - anchors.rightMargin: 6 - anchors.topMargin: 6 - visible: parent.hasUnread - } - - MouseArea { - id: notificationArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: { - root.notificationHistoryVisible = !root.notificationHistoryVisible - } - } - - Behavior on color { - ColorAnimation { - duration: theme.shortDuration - easing.type: theme.standardEasing - } - } - } - } - } - } - - PanelWindow { - id: calendarPopup - - visible: root.calendarVisible - - implicitWidth: 320 - implicitHeight: 400 - - WlrLayershell.layer: WlrLayershell.Overlay - WlrLayershell.exclusiveZone: -1 - WlrLayershell.keyboardFocus: WlrKeyboardFocus.None - - color: "transparent" - - anchors { - top: true - left: true - right: true - bottom: true - } - - property date displayDate: new Date() - property date selectedDate: new Date() - - Rectangle { - width: 400 - height: root.weather.available ? 480 : 400 - x: (parent.width - width) / 2 - y: theme.barHeight + theme.spacingS - color: theme.surfaceContainer - radius: theme.cornerRadiusLarge - border.color: Qt.rgba(theme.outline.r, theme.outline.g, theme.outline.b, 0.12) - border.width: 1 - - opacity: root.calendarVisible ? 1.0 : 0.0 - scale: root.calendarVisible ? 1.0 : 0.85 - - Behavior on opacity { - NumberAnimation { - duration: theme.mediumDuration - easing.type: theme.emphasizedEasing - } - } - - Behavior on scale { - NumberAnimation { - duration: theme.mediumDuration - easing.type: theme.emphasizedEasing - } - } - - Column { - anchors.fill: parent - anchors.margins: theme.spacingL - spacing: theme.spacingM - - // Weather header (when available) - Rectangle { - visible: root.weather.available - width: parent.width - height: 80 - radius: theme.cornerRadius - color: Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.08) - border.color: Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.2) - border.width: 1 - - Row { - anchors.centerIn: parent - spacing: theme.spacingL - - // Weather icon and temp - Column { - spacing: 2 - anchors.verticalCenter: parent.verticalCenter - - Text { - text: root.weatherIcons[root.weather.wCode] || "clear_day" - font.family: theme.iconFont - font.pixelSize: theme.iconSize + 4 - color: theme.primary - anchors.horizontalCenter: parent.horizontalCenter - } - - Text { - text: (root.useFahrenheit ? root.weather.tempF : root.weather.temp) + "°" + (root.useFahrenheit ? "F" : "C") - font.pixelSize: theme.fontSizeLarge - color: theme.surfaceText - font.weight: Font.Bold - anchors.horizontalCenter: parent.horizontalCenter - - MouseArea { - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: root.useFahrenheit = !root.useFahrenheit - } - } - - Text { - text: root.weather.city - font.pixelSize: theme.fontSizeSmall - color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.7) - anchors.horizontalCenter: parent.horizontalCenter - } - } - - // Weather details grid - Grid { - columns: 2 - spacing: theme.spacingS - anchors.verticalCenter: parent.verticalCenter - - Row { - spacing: theme.spacingXS - Text { - text: "humidity_low" - font.family: theme.iconFont - font.pixelSize: theme.fontSizeSmall - color: theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - Text { - text: root.weather.humidity + "%" - font.pixelSize: theme.fontSizeSmall - color: theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - } - - Row { - spacing: theme.spacingXS - Text { - text: "air" - font.family: theme.iconFont - font.pixelSize: theme.fontSizeSmall - color: theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - Text { - text: root.weather.wind - font.pixelSize: theme.fontSizeSmall - color: theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - } - - Row { - spacing: theme.spacingXS - Text { - text: "wb_twilight" - font.family: theme.iconFont - font.pixelSize: theme.fontSizeSmall - color: theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - Text { - text: root.weather.sunrise - font.pixelSize: theme.fontSizeSmall - color: theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - } - - Row { - spacing: theme.spacingXS - Text { - text: "bedtime" - font.family: theme.iconFont - font.pixelSize: theme.fontSizeSmall - color: theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - Text { - text: root.weather.sunset - font.pixelSize: theme.fontSizeSmall - color: theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - } - } - } - } - - Row { - width: parent.width - height: 40 - - Rectangle { - width: 40 - height: 40 - radius: theme.cornerRadius - color: prevMonthArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) : "transparent" - - Text { - anchors.centerIn: parent - text: "chevron_left" - font.family: theme.iconFont - font.pixelSize: theme.iconSize - color: theme.primary - font.weight: theme.iconFontWeight - } - - MouseArea { - id: prevMonthArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: { - let newDate = new Date(calendarPopup.displayDate) - newDate.setMonth(newDate.getMonth() - 1) - calendarPopup.displayDate = newDate - } - } - } - - Text { - width: parent.width - 80 - height: 40 - text: Qt.formatDate(calendarPopup.displayDate, "MMMM yyyy") - font.pixelSize: theme.fontSizeLarge - color: theme.surfaceText - font.weight: Font.Medium - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - - Rectangle { - width: 40 - height: 40 - radius: theme.cornerRadius - color: nextMonthArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) : "transparent" - - Text { - anchors.centerIn: parent - text: "chevron_right" - font.family: theme.iconFont - font.pixelSize: theme.iconSize - color: theme.primary - font.weight: theme.iconFontWeight - } - - MouseArea { - id: nextMonthArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: { - let newDate = new Date(calendarPopup.displayDate) - newDate.setMonth(newDate.getMonth() + 1) - calendarPopup.displayDate = newDate - } - } - } - } - - Row { - width: parent.width - height: 32 - - Repeater { - model: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] - - Rectangle { - width: parent.width / 7 - height: 32 - color: "transparent" - - Text { - anchors.centerIn: parent - text: modelData - font.pixelSize: theme.fontSizeSmall - color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.6) - font.weight: Font.Medium - } - } - } - } - - Grid { - width: parent.width - height: root.weather.available ? parent.height - 200 : parent.height - 120 - columns: 7 - rows: 6 - - property date firstDay: { - let date = new Date(calendarPopup.displayDate.getFullYear(), calendarPopup.displayDate.getMonth(), 1) - let dayOfWeek = date.getDay() - date.setDate(date.getDate() - dayOfWeek) - return date - } - - Repeater { - model: 42 - - Rectangle { - width: parent.width / 7 - height: parent.height / 6 - - property date dayDate: { - let date = new Date(parent.firstDay) - date.setDate(date.getDate() + index) - return date - } - - property bool isCurrentMonth: dayDate.getMonth() === calendarPopup.displayDate.getMonth() - property bool isToday: dayDate.toDateString() === new Date().toDateString() - property bool isSelected: dayDate.toDateString() === calendarPopup.selectedDate.toDateString() - - color: isSelected ? theme.primary : - isToday ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) : - dayArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.08) : "transparent" - - radius: theme.cornerRadiusSmall - - Text { - anchors.centerIn: parent - text: dayDate.getDate() - font.pixelSize: theme.fontSizeMedium - color: isSelected ? theme.surface : - isToday ? theme.primary : - isCurrentMonth ? theme.surfaceText : - Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.4) - font.weight: isToday || isSelected ? Font.Medium : Font.Normal - } - - MouseArea { - id: dayArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: { - calendarPopup.selectedDate = dayDate - } - } - } - } - } - } - } - - MouseArea { - anchors.fill: parent - z: -1 - onClicked: { - root.calendarVisible = false - } - } - } - - // Custom Material 3 System Tray Menu - PanelWindow { - id: trayMenuPopup - - visible: root.showTrayMenu - - WlrLayershell.layer: WlrLayershell.Overlay - WlrLayershell.exclusiveZone: -1 - WlrLayershell.keyboardFocus: WlrKeyboardFocus.None - - color: "transparent" - - anchors { - top: true - left: true - right: true - bottom: true - } - - Rectangle { - id: menuContainer - x: root.trayMenuX - y: root.trayMenuY - width: 180 - height: Math.max(60, menuList.contentHeight + theme.spacingS * 2) - color: theme.surfaceContainer - radius: theme.cornerRadiusLarge - border.color: Qt.rgba(theme.outline.r, theme.outline.g, theme.outline.b, 0.12) - border.width: 1 - - // Material 3 drop shadow - Rectangle { - anchors.fill: parent - anchors.topMargin: 4 - anchors.leftMargin: 2 - anchors.rightMargin: -2 - anchors.bottomMargin: -4 - radius: parent.radius - color: Qt.rgba(0, 0, 0, 0.15) - z: parent.z - 1 - } - - // Material 3 animations - opacity: root.showTrayMenu ? 1.0 : 0.0 - scale: root.showTrayMenu ? 1.0 : 0.85 - - Behavior on opacity { - NumberAnimation { - duration: theme.mediumDuration - easing.type: theme.emphasizedEasing - } - } - - Behavior on scale { - NumberAnimation { - duration: theme.mediumDuration - easing.type: theme.emphasizedEasing - } - } - - Item { - anchors.fill: parent - anchors.margins: theme.spacingS - - QsMenuOpener { - id: menuOpener - menu: root.currentTrayItem?.menu - } - - // Custom menu styling using ListView - ListView { - id: menuList - anchors.fill: parent - spacing: 1 - model: ScriptModel { - values: menuOpener.children ? [...menuOpener.children.values].filter(item => { - // Filter out empty items and separators - return item && item.text && item.text.trim().length > 0 && !item.isSeparator - }) : [] - } - - delegate: Rectangle { - width: ListView.view.width - height: modelData.isSeparator ? 5 : 28 - radius: modelData.isSeparator ? 0 : theme.cornerRadiusSmall - color: modelData.isSeparator ? "transparent" : - (menuItemArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) : "transparent") - - // Separator line - Rectangle { - visible: modelData.isSeparator - anchors.centerIn: parent - width: parent.width - theme.spacingS * 2 - height: 1 - color: Qt.rgba(theme.outline.r, theme.outline.g, theme.outline.b, 0.2) - } - - // Menu item content - Row { - visible: !modelData.isSeparator - anchors.left: parent.left - anchors.leftMargin: theme.spacingS - anchors.verticalCenter: parent.verticalCenter - spacing: theme.spacingXS - - Text { - text: modelData.text || "" - font.pixelSize: theme.fontSizeSmall - color: theme.surfaceText - font.weight: Font.Normal - } - } - - MouseArea { - id: menuItemArea - anchors.fill: parent - hoverEnabled: true - cursorShape: modelData.isSeparator ? Qt.ArrowCursor : Qt.PointingHandCursor - enabled: !modelData.isSeparator - - onClicked: { - if (modelData.triggered) { - modelData.triggered() - } - root.showTrayMenu = false - } - } - - Behavior on color { - ColorAnimation { - duration: theme.shortDuration - easing.type: theme.standardEasing - } - } - } - } - } - } - - // Click outside to close - MouseArea { - anchors.fill: parent - z: -1 - onClicked: { - root.showTrayMenu = false - } - } - } - - // Notification Popup (using PanelWindow like reference) - PanelWindow { - id: notificationPopup - - visible: root.showNotificationPopup && root.activeNotification - - WlrLayershell.layer: WlrLayershell.Overlay - WlrLayershell.exclusiveZone: -1 - WlrLayershell.keyboardFocus: WlrKeyboardFocus.None - - color: "transparent" - - anchors { - top: true - right: true - bottom: true - } - - implicitWidth: 400 - - Rectangle { - id: popupContainer - width: 380 - height: 100 - anchors.top: parent.top - anchors.right: parent.right - anchors.topMargin: theme.barHeight + 16 - anchors.rightMargin: 16 - - color: theme.surfaceContainer - radius: theme.cornerRadiusLarge - border.color: Qt.rgba(theme.outline.r, theme.outline.g, theme.outline.b, 0.2) - border.width: 1 - - opacity: root.showNotificationPopup ? 1.0 : 0.0 - - Behavior on opacity { - NumberAnimation { duration: 200; easing.type: Easing.OutQuad } - } - - MouseArea { - anchors.fill: parent - onClicked: hideNotificationPopup() - } - - // Close button with cursor pointer - Text { - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: 8 - text: "×" - font.pixelSize: 16 - color: theme.surfaceText - - MouseArea { - anchors.fill: parent - anchors.margins: -4 - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: hideNotificationPopup() - } - } - - // Content layout - Row { - anchors.fill: parent - anchors.margins: 12 - anchors.rightMargin: 32 - spacing: 12 - - // Notification icon using reference pattern - Rectangle { - width: 40 - height: 40 - radius: 8 - color: Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.1) - anchors.verticalCenter: parent.verticalCenter - - // Fallback material icon when no app icon - Loader { - active: !root.activeNotification || root.activeNotification.appIcon === "" - anchors.fill: parent - sourceComponent: Text { - anchors.centerIn: parent - text: "notifications" - font.family: theme.iconFont - font.pixelSize: 20 - color: theme.primary - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - } - - // App icon when no notification image - Loader { - active: root.activeNotification && root.activeNotification.appIcon !== "" && (root.activeNotification.image === "" || !root.activeNotification.image) - anchors.centerIn: parent - sourceComponent: IconImage { - width: 32 - height: 32 - asynchronous: true - source: { - if (!root.activeNotification) return "" - let iconPath = root.activeNotification.appIcon - // Skip file:// URLs as they're usually screenshots/images, not icons - if (iconPath && iconPath.startsWith("file://")) return "" - return iconPath ? Quickshell.iconPath(iconPath, "image-missing") : "" - } - } - } - - // Notification image with rounded corners - Loader { - active: root.activeNotification && root.activeNotification.image !== "" - anchors.fill: parent - sourceComponent: Item { - anchors.fill: parent - Image { - id: notifImage - anchors.fill: parent - source: root.activeNotification ? root.activeNotification.image : "" - fillMode: Image.PreserveAspectCrop - cache: false - antialiasing: true - asynchronous: true - - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - width: notifImage.width - height: notifImage.height - radius: 8 - } - } - } - - // Small app icon overlay when showing notification image - Loader { - active: root.activeNotification && root.activeNotification.appIcon !== "" - anchors.bottom: parent.bottom - anchors.right: parent.right - anchors.margins: 2 - sourceComponent: IconImage { - width: 16 - height: 16 - asynchronous: true - source: root.activeNotification ? Quickshell.iconPath(root.activeNotification.appIcon, "image-missing") : "" - } - } - } - } - } - - // Text content - Column { - width: parent.width - 52 - anchors.verticalCenter: parent.verticalCenter - spacing: 4 - - Text { - text: root.activeNotification ? (root.activeNotification.summary || "") : "" - font.pixelSize: 14 - color: theme.surfaceText - font.weight: Font.Medium - width: parent.width - elide: Text.ElideRight - visible: text.length > 0 - } - - Text { - text: root.activeNotification ? (root.activeNotification.body || "") : "" - font.pixelSize: 12 - color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.7) - width: parent.width - wrapMode: Text.WordWrap - maximumLineCount: 2 - elide: Text.ElideRight - visible: text.length > 0 - } - } - } - } - } - - // Auto-hide notification popup timer - Timer { - id: notificationTimer - interval: 5000 // 5 seconds - repeat: false - onTriggered: hideNotificationPopup() - } - - // Timer for clearing active notification after animation - Timer { - id: clearNotificationTimer - interval: theme.mediumDuration + 50 - repeat: false - onTriggered: root.activeNotification = null - } - - function showNotificationPopup(notification) { - root.activeNotification = notification - root.showNotificationPopup = true - notificationTimer.restart() - } - - function hideNotificationPopup() { - root.showNotificationPopup = false - notificationTimer.stop() - clearNotificationTimer.restart() - } - - // Notification History Panel - PanelWindow { - id: notificationHistoryPopup - - visible: root.notificationHistoryVisible - - implicitWidth: 400 - implicitHeight: 500 - - WlrLayershell.layer: WlrLayershell.Overlay - WlrLayershell.exclusiveZone: -1 - WlrLayershell.keyboardFocus: WlrKeyboardFocus.None - - color: "transparent" - - anchors { - top: true - left: true - right: true - bottom: true - } - - Rectangle { - width: 400 - height: 500 - x: parent.width - width - theme.spacingL - y: theme.barHeight + theme.spacingS - color: theme.surfaceContainer - radius: theme.cornerRadiusLarge - border.color: Qt.rgba(theme.outline.r, theme.outline.g, theme.outline.b, 0.12) - border.width: 1 - - opacity: root.notificationHistoryVisible ? 1.0 : 0.0 - scale: root.notificationHistoryVisible ? 1.0 : 0.85 - - Behavior on opacity { - NumberAnimation { - duration: theme.mediumDuration - easing.type: theme.emphasizedEasing - } - } - - Behavior on scale { - NumberAnimation { - duration: theme.mediumDuration - easing.type: theme.emphasizedEasing - } - } - - Column { - anchors.fill: parent - anchors.margins: theme.spacingL - spacing: theme.spacingM - - // Header - Column { - width: parent.width - spacing: theme.spacingM - - Row { - width: parent.width - height: 32 - - Text { - text: "Notifications" - font.pixelSize: theme.fontSizeLarge - color: theme.surfaceText - font.weight: Font.Medium - anchors.verticalCenter: parent.verticalCenter - } - - Item { width: parent.width - 200; height: 1 } - } - - Rectangle { - width: parent.width - height: 36 - radius: theme.cornerRadius - color: clearArea.containsMouse ? Qt.rgba(theme.error.r, theme.error.g, theme.error.b, 0.16) : Qt.rgba(theme.error.r, theme.error.g, theme.error.b, 0.12) - border.color: Qt.rgba(theme.error.r, theme.error.g, theme.error.b, 0.5) - border.width: 1 - visible: notificationHistory.count > 0 - - Row { - anchors.centerIn: parent - spacing: theme.spacingS - - Text { - text: "delete_sweep" - font.family: theme.iconFont - font.pixelSize: theme.iconSizeSmall + 2 - color: theme.error - font.weight: theme.iconFontWeight - anchors.verticalCenter: parent.verticalCenter - } - - Text { - text: "Clear All Notifications" - font.pixelSize: theme.fontSizeMedium - color: theme.error - font.weight: Font.Medium - anchors.verticalCenter: parent.verticalCenter - } - } - - MouseArea { - id: clearArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: { - notificationHistory.clear() - } - } - - Behavior on color { - ColorAnimation { - duration: theme.shortDuration - easing.type: theme.standardEasing - } - } - - Behavior on border.color { - ColorAnimation { - duration: theme.shortDuration - easing.type: theme.standardEasing - } - } - } - } - - // Notification List - ScrollView { - width: parent.width - height: parent.height - 120 - clip: true - - ListView { - id: notificationListView - model: notificationHistory - spacing: theme.spacingS - - delegate: Rectangle { - width: notificationListView.width - height: 80 - radius: theme.cornerRadius - color: notifArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.08) : Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.08) - - Row { - anchors.fill: parent - anchors.margins: theme.spacingM - spacing: theme.spacingM - - // Notification icon using reference pattern - Rectangle { - width: 32 - height: 32 - radius: theme.cornerRadius - color: Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) - anchors.verticalCenter: parent.verticalCenter - - // Fallback material icon when no app icon - Loader { - active: !model.appIcon || model.appIcon === "" - anchors.fill: parent - sourceComponent: Text { - anchors.centerIn: parent - text: model.appName ? model.appName.charAt(0).toUpperCase() : "notifications" - font.family: model.appName ? "Roboto" : theme.iconFont - font.pixelSize: model.appName ? theme.fontSizeMedium : 16 - color: theme.primary - font.weight: Font.Medium - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - } - - // App icon when no notification image - Loader { - active: model.appIcon && model.appIcon !== "" && (!model.image || model.image === "") - anchors.centerIn: parent - sourceComponent: IconImage { - width: 28 - height: 28 - asynchronous: true - source: { - if (!model.appIcon) return "" - // Skip file:// URLs as they're usually screenshots/images, not icons - if (model.appIcon.startsWith("file://")) return "" - return Quickshell.iconPath(model.appIcon, "image-missing") - } - } - } - - // Notification image with rounded corners - Loader { - active: model.image && model.image !== "" - anchors.fill: parent - sourceComponent: Item { - anchors.fill: parent - Image { - id: historyNotifImage - anchors.fill: parent - source: model.image || "" - fillMode: Image.PreserveAspectCrop - cache: false - antialiasing: true - asynchronous: true - - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - width: historyNotifImage.width - height: historyNotifImage.height - radius: theme.cornerRadius - } - } - } - - // Small app icon overlay when showing notification image - Loader { - active: model.appIcon && model.appIcon !== "" - anchors.bottom: parent.bottom - anchors.right: parent.right - anchors.margins: 2 - sourceComponent: IconImage { - width: 12 - height: 12 - asynchronous: true - source: model.appIcon ? Quickshell.iconPath(model.appIcon, "image-missing") : "" - } - } - } - } - } - - // Content - Column { - anchors.verticalCenter: parent.verticalCenter - width: parent.width - 80 - spacing: theme.spacingXS - - Text { - text: model.appName || "App" - font.pixelSize: theme.fontSizeSmall - color: theme.primary - font.weight: Font.Medium - } - - Text { - text: model.summary || "" - font.pixelSize: theme.fontSizeMedium - color: theme.surfaceText - font.weight: Font.Medium - width: parent.width - elide: Text.ElideRight - visible: text.length > 0 - } - - Text { - text: model.body || "" - font.pixelSize: theme.fontSizeSmall - color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.7) - width: parent.width - wrapMode: Text.WordWrap - maximumLineCount: 2 - elide: Text.ElideRight - visible: text.length > 0 - } - } - } - - MouseArea { - id: notifArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: { - notificationHistory.remove(index) - } - } - - Behavior on color { - ColorAnimation { - duration: theme.shortDuration - easing.type: theme.standardEasing - } - } - } - } - - // Empty state - properly centered - Rectangle { - anchors.fill: parent - visible: notificationHistory.count === 0 - color: "transparent" - - Column { - anchors.centerIn: parent - spacing: theme.spacingM - width: parent.width * 0.8 - - Text { - anchors.horizontalCenter: parent.horizontalCenter - text: "notifications_none" - font.family: theme.iconFont - font.pixelSize: theme.iconSizeLarge + 16 - color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.3) - font.weight: theme.iconFontWeight - } - - Text { - anchors.horizontalCenter: parent.horizontalCenter - text: "No notifications" - font.pixelSize: theme.fontSizeLarge - color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.6) - font.weight: Font.Medium - horizontalAlignment: Text.AlignHCenter - } - - Text { - anchors.horizontalCenter: parent.horizontalCenter - text: "Notifications will appear here" - font.pixelSize: theme.fontSizeMedium - color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.4) - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - width: parent.width - } - } - } - } - } - } - - // Click outside to close - MouseArea { - anchors.fill: parent - z: -1 - onClicked: { - root.notificationHistoryVisible = false - } - } - } - - AppLauncher { - id: appLauncher - theme: root.theme - } - - ClipboardHistory { - id: clipboardHistoryPopup - theme: root.theme - } - - // OS Detection - Process { - id: osDetector - command: ["lsb_release", "-i", "-s"] - running: true - - stdout: SplitParser { - splitMarker: "\n" - onRead: (data) => { - if (data.trim()) { - let osId = data.trim().toLowerCase() - console.log("Detected OS:", osId) - - // Set OS-specific Nerd Font icons and names - if (osId.includes("arch")) { - root.osLogo = "\uf303" // Arch Linux Nerd Font icon - root.osName = "Arch Linux" - console.log("Set Arch logo:", root.osLogo) - } else if (osId.includes("ubuntu")) { - root.osLogo = "\uf31b" // Ubuntu Nerd Font icon - root.osName = "Ubuntu" - } else if (osId.includes("fedora")) { - root.osLogo = "\uf30a" // Fedora Nerd Font icon - root.osName = "Fedora" - } else if (osId.includes("debian")) { - root.osLogo = "\uf306" // Debian Nerd Font icon - root.osName = "Debian" - } else if (osId.includes("opensuse")) { - root.osLogo = "\uef6d" // openSUSE Nerd Font icon - root.osName = "openSUSE" - } else if (osId.includes("manjaro")) { - root.osLogo = "\uf312" // Manjaro Nerd Font icon - root.osName = "Manjaro" - } else { - root.osLogo = "\uf033" // Generic Linux Nerd Font icon - root.osName = "Linux" - } - } - } - } - - onExited: (exitCode) => { - if (exitCode !== 0) { - // Fallback: try checking /etc/os-release - osDetectorFallback.running = true - } - } - } - - // Fallback OS detection - Process { - id: osDetectorFallback - command: ["sh", "-c", "grep '^ID=' /etc/os-release | cut -d'=' -f2 | tr -d '\"'"] - running: false - - stdout: SplitParser { - splitMarker: "\n" - onRead: (data) => { - if (data.trim()) { - let osId = data.trim().toLowerCase() - console.log("Detected OS (fallback):", osId) - - if (osId.includes("arch")) { - root.osLogo = "\uf303" - root.osName = "Arch Linux" - } else if (osId.includes("ubuntu")) { - root.osLogo = "\uf31b" - root.osName = "Ubuntu" - } else if (osId.includes("fedora")) { - root.osLogo = "\uf30a" - root.osName = "Fedora" - } else if (osId.includes("debian")) { - root.osLogo = "\uf306" - root.osName = "Debian" - } else if (osId.includes("opensuse")) { - root.osLogo = "\uef6d" - root.osName = "openSUSE" - } else if (osId.includes("manjaro")) { - root.osLogo = "\uf312" - root.osName = "Manjaro" - } else { - root.osLogo = "\uf033" - root.osName = "Linux" - } - } - } - } - - onExited: (exitCode) => { - if (exitCode !== 0) { - // Ultimate fallback - use generic apps icon (empty logo means fallback to "apps") - root.osLogo = "" - root.osName = "Linux" - console.log("OS detection failed, using generic icon") - } - } - } - - - // Color Picker Process - Process { - id: colorPickerProcess - command: ["hyprpicker", "-a"] - running: false - - onExited: (exitCode) => { - if (exitCode !== 0) { - console.warn("Color picker failed. Make sure hyprpicker is installed: yay -S hyprpicker") - } - } - } - - // Notification Server - NotificationServer { - id: notificationServer - actionsSupported: true - bodyMarkupSupported: true - imageSupported: true - keepOnReload: false - persistenceSupported: true - - onNotification: (notification) => { - if (!notification || !notification.id) return - - // Filter empty notifications - if (!notification.appName && !notification.summary && !notification.body) { - return - } - - console.log("New notification from:", notification.appName || "Unknown", "Summary:", notification.summary || "No summary") - - // Create notification object with correct properties - var notifObj = { - "id": notification.id, - "appName": notification.appName || "App", - "summary": notification.summary || "", - "body": notification.body || "", - "timestamp": new Date(), - "appIcon": notification.appIcon || notification.icon || "", - "icon": notification.icon || "", - "image": notification.image || "" - } - - // Add to history (prepend to show newest first) - notificationHistory.insert(0, notifObj) - - // Keep only last 50 notifications - while (notificationHistory.count > 50) { - notificationHistory.remove(notificationHistory.count - 1) - } - - // Show popup notification - root.activeNotification = notifObj - root.showNotificationPopup = true - notificationTimer.restart() - } - } - - // Notification History Model - ListModel { - id: notificationHistory - } - - // Weather Service - Process { - id: weatherFetcher - command: ["bash", "-c", "curl -s 'wttr.in/?format=j1' | jq '{current: .current_condition[0], location: .nearest_area[0], astronomy: .weather[0].astronomy[0]}'"] - running: false - - stdout: StdioCollector { - onStreamFinished: { - if (text.trim() && text.trim().startsWith("{")) { - try { - let parsedData = JSON.parse(text.trim()) - if (parsedData.current && parsedData.location) { - root.weather = { - available: true, - temp: parseInt(parsedData.current.temp_C || 0), - tempF: parseInt(parsedData.current.temp_F || 0), - city: parsedData.location.areaName[0]?.value || "Unknown", - wCode: parsedData.current.weatherCode || "113", - humidity: parseInt(parsedData.current.humidity || 0), - wind: (parsedData.current.windspeedKmph || 0) + " km/h", - sunrise: parsedData.astronomy?.sunrise || "06:00", - sunset: parsedData.astronomy?.sunset || "18:00", - uv: parseInt(parsedData.current.uvIndex || 0), - pressure: parseInt(parsedData.current.pressure || 0) - } - console.log("Weather updated:", root.weather.city, root.weather.temp + "°C") - } - } catch (e) { - console.warn("Failed to parse weather data:", e.message) - root.weather.available = false - } - } else { - console.warn("No valid weather data received") - root.weather.available = false - } - } - } - - onExited: (exitCode) => { - if (exitCode !== 0) { - console.warn("Weather fetch failed with exit code:", exitCode) - root.weather.available = false - } - } - } - - // Weather fetch timer (every 10 minutes) - Timer { - interval: 600000 // 10 minutes - running: true - repeat: true - triggeredOnStart: true - onTriggered: { - weatherFetcher.running = true - } - } - - - Timer { - running: root.activePlayer?.playbackState === MprisPlaybackState.Playing - interval: 1000 - repeat: true - onTriggered: { - if (root.activePlayer) { - root.activePlayer.positionChanged() - } - } - } - - Component.onCompleted: { - console.log("DankMaterialDark shell loaded successfully!") - } -} \ No newline at end of file diff --git a/shell.qml b/shell.qml index 228d7c7e..8888d6bf 100644 --- a/shell.qml +++ b/shell.qml @@ -215,18 +215,28 @@ ShellRoot { "395": "snowing" }) - // Top bar - PanelWindow { - id: topBar - - anchors { - top: true - left: true - right: true - } - - implicitHeight: theme.barHeight - color: "transparent" + // Top bar - one instance per screen + Variants { + model: Quickshell.screens + + PanelWindow { + id: topBar + + // modelData contains the screen from Quickshell.screens + property var modelData + screen: modelData + + // Get the screen name (e.g., "DP-1", "DP-2") + property string screenName: modelData.name + + anchors { + top: true + left: true + right: true + } + + implicitHeight: theme.barHeight + color: "transparent" Rectangle { anchors.fill: parent @@ -341,11 +351,10 @@ ShellRoot { command: ["niri", "msg", "workspaces"] running: true - stdout: SplitParser { - splitMarker: "\n" - onRead: (data) => { - if (data.trim()) { - workspaceSwitcher.parseWorkspaceOutput(data.trim()) + stdout: StdioCollector { + onStreamFinished: { + if (text && text.trim()) { + workspaceSwitcher.parseWorkspaceOutput(text.trim()) } } } @@ -358,6 +367,7 @@ ShellRoot { let focusedWorkspace = 1 let outputWorkspaces = {} + for (const line of lines) { if (line.startsWith('Output "')) { const outputMatch = line.match(/Output "(.+)"/) @@ -386,12 +396,37 @@ ShellRoot { } } - currentWorkspace = focusedWorkspace - - if (focusedOutput && outputWorkspaces[focusedOutput]) { - workspaceList = outputWorkspaces[focusedOutput] + // Show workspaces for THIS screen only + if (topBar.screenName && outputWorkspaces[topBar.screenName]) { + workspaceList = outputWorkspaces[topBar.screenName] + + // Always track the active workspace for this display + // Parse all lines to find which workspace is active on this display + let thisDisplayActiveWorkspace = 1 + let inThisOutput = false + + for (const line of lines) { + if (line.startsWith('Output "')) { + const outputMatch = line.match(/Output "(.+)"/) + inThisOutput = outputMatch && outputMatch[1] === topBar.screenName + continue + } + + if (inThisOutput && line.trim() && line.match(/^\s*\*\s*(\d+)$/)) { + const wsMatch = line.match(/^\s*\*\s*(\d+)$/) + if (wsMatch) { + thisDisplayActiveWorkspace = parseInt(wsMatch[1]) + break + } + } + } + + currentWorkspace = thisDisplayActiveWorkspace + console.log("Monitor", topBar.screenName, "active workspace:", thisDisplayActiveWorkspace) } else { + // Fallback if screen name not found workspaceList = [1, 2] + currentWorkspace = 1 } } @@ -444,12 +479,11 @@ ShellRoot { cursorShape: Qt.PointingHandCursor onClicked: { - switchProcess.command = ["niri", "msg", "action", "focus-workspace", modelData.toString()] - switchProcess.running = true - workspaceSwitcher.currentWorkspace = modelData - Qt.callLater(() => { - workspaceQuery.running = true - }) + // Set target workspace and focus monitor first + console.log("Clicking workspace", modelData, "on monitor", topBar.screenName) + workspaceSwitcher.targetWorkspace = modelData + focusMonitorProcess.command = ["niri", "msg", "action", "focus-monitor", topBar.screenName] + focusMonitorProcess.running = true } } } @@ -459,7 +493,30 @@ ShellRoot { Process { id: switchProcess running: false + + onExited: { + // Update current workspace and refresh query + workspaceSwitcher.currentWorkspace = workspaceSwitcher.targetWorkspace + Qt.callLater(() => { + workspaceQuery.running = true + }) + } } + + Process { + id: focusMonitorProcess + running: false + + onExited: { + // After focusing the monitor, switch to the workspace + Qt.callLater(() => { + switchProcess.command = ["niri", "msg", "action", "focus-workspace", workspaceSwitcher.targetWorkspace.toString()] + switchProcess.running = true + }) + } + } + + property int targetWorkspace: 1 } } @@ -954,6 +1011,7 @@ ShellRoot { } } } + } // End of Variants for topBar PanelWindow { id: calendarPopup