mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-05 21:15:38 -05:00
Fix multi-monitor
This commit is contained in:
110
CLAUDE.md
Normal file
110
CLAUDE.md
Normal file
@@ -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
|
||||||
@@ -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!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
269
shell-test.qml
269
shell-test.qml
@@ -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!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2246
shell-working.qml
2246
shell-working.qml
File diff suppressed because it is too large
Load Diff
112
shell.qml
112
shell.qml
@@ -215,18 +215,28 @@ ShellRoot {
|
|||||||
"395": "snowing"
|
"395": "snowing"
|
||||||
})
|
})
|
||||||
|
|
||||||
// Top bar
|
// Top bar - one instance per screen
|
||||||
PanelWindow {
|
Variants {
|
||||||
id: topBar
|
model: Quickshell.screens
|
||||||
|
|
||||||
anchors {
|
PanelWindow {
|
||||||
top: true
|
id: topBar
|
||||||
left: true
|
|
||||||
right: true
|
// modelData contains the screen from Quickshell.screens
|
||||||
}
|
property var modelData
|
||||||
|
screen: modelData
|
||||||
implicitHeight: theme.barHeight
|
|
||||||
color: "transparent"
|
// 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 {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -341,11 +351,10 @@ ShellRoot {
|
|||||||
command: ["niri", "msg", "workspaces"]
|
command: ["niri", "msg", "workspaces"]
|
||||||
running: true
|
running: true
|
||||||
|
|
||||||
stdout: SplitParser {
|
stdout: StdioCollector {
|
||||||
splitMarker: "\n"
|
onStreamFinished: {
|
||||||
onRead: (data) => {
|
if (text && text.trim()) {
|
||||||
if (data.trim()) {
|
workspaceSwitcher.parseWorkspaceOutput(text.trim())
|
||||||
workspaceSwitcher.parseWorkspaceOutput(data.trim())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -358,6 +367,7 @@ ShellRoot {
|
|||||||
let focusedWorkspace = 1
|
let focusedWorkspace = 1
|
||||||
let outputWorkspaces = {}
|
let outputWorkspaces = {}
|
||||||
|
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line.startsWith('Output "')) {
|
if (line.startsWith('Output "')) {
|
||||||
const outputMatch = line.match(/Output "(.+)"/)
|
const outputMatch = line.match(/Output "(.+)"/)
|
||||||
@@ -386,12 +396,37 @@ ShellRoot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentWorkspace = focusedWorkspace
|
// Show workspaces for THIS screen only
|
||||||
|
if (topBar.screenName && outputWorkspaces[topBar.screenName]) {
|
||||||
if (focusedOutput && outputWorkspaces[focusedOutput]) {
|
workspaceList = outputWorkspaces[topBar.screenName]
|
||||||
workspaceList = outputWorkspaces[focusedOutput]
|
|
||||||
|
// 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 {
|
} else {
|
||||||
|
// Fallback if screen name not found
|
||||||
workspaceList = [1, 2]
|
workspaceList = [1, 2]
|
||||||
|
currentWorkspace = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,12 +479,11 @@ ShellRoot {
|
|||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
switchProcess.command = ["niri", "msg", "action", "focus-workspace", modelData.toString()]
|
// Set target workspace and focus monitor first
|
||||||
switchProcess.running = true
|
console.log("Clicking workspace", modelData, "on monitor", topBar.screenName)
|
||||||
workspaceSwitcher.currentWorkspace = modelData
|
workspaceSwitcher.targetWorkspace = modelData
|
||||||
Qt.callLater(() => {
|
focusMonitorProcess.command = ["niri", "msg", "action", "focus-monitor", topBar.screenName]
|
||||||
workspaceQuery.running = true
|
focusMonitorProcess.running = true
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -459,7 +493,30 @@ ShellRoot {
|
|||||||
Process {
|
Process {
|
||||||
id: switchProcess
|
id: switchProcess
|
||||||
running: false
|
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 {
|
PanelWindow {
|
||||||
id: calendarPopup
|
id: calendarPopup
|
||||||
|
|||||||
Reference in New Issue
Block a user