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
106
shell.qml
106
shell.qml
@@ -215,18 +215,28 @@ ShellRoot {
|
||||
"395": "snowing"
|
||||
})
|
||||
|
||||
// Top bar
|
||||
PanelWindow {
|
||||
id: topBar
|
||||
// Top bar - one instance per screen
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
PanelWindow {
|
||||
id: topBar
|
||||
|
||||
implicitHeight: theme.barHeight
|
||||
color: "transparent"
|
||||
// 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
|
||||
// Show workspaces for THIS screen only
|
||||
if (topBar.screenName && outputWorkspaces[topBar.screenName]) {
|
||||
workspaceList = outputWorkspaces[topBar.screenName]
|
||||
|
||||
if (focusedOutput && outputWorkspaces[focusedOutput]) {
|
||||
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 {
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user