1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-28 23:42:51 -05:00

event based workspace switches

This commit is contained in:
bbedward
2025-07-11 21:36:42 -04:00
parent eb6df3318a
commit 483ce1cd93
4 changed files with 380 additions and 211 deletions

View File

@@ -0,0 +1,288 @@
import QtQuick
import Quickshell
import Quickshell.Io
pragma Singleton
pragma ComponentBehavior: Bound
Singleton {
id: root
// Workspace management
property list<var> allWorkspaces: []
property int focusedWorkspaceIndex: 0
property string focusedWorkspaceId: ""
property var currentOutputWorkspaces: []
property string currentOutput: ""
// Window management
property list<var> windows: []
property int focusedWindowIndex: -1
property string focusedWindowTitle: "(No active window)"
property string focusedWindowId: ""
// Overview state
property bool inOverview: false
// Feature availability
property bool niriAvailable: false
Component.onCompleted: {
console.log("NiriWorkspaceService: Component.onCompleted - initializing service")
checkNiriAvailability()
}
// Check if niri is available
Process {
id: niriCheck
command: ["which", "niri"]
onExited: (exitCode) => {
root.niriAvailable = exitCode === 0
if (root.niriAvailable) {
console.log("NiriWorkspaceService: niri found, starting event stream and loading initial data")
eventStreamProcess.running = true
loadInitialWorkspaceData()
} else {
console.log("NiriWorkspaceService: niri not found, workspace features disabled")
}
}
}
function checkNiriAvailability() {
niriCheck.running = true
}
// Load initial workspace data
Process {
id: initialDataQuery
command: ["niri", "msg", "-j", "workspaces"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text && text.trim()) {
try {
console.log("NiriWorkspaceService: Loaded initial workspace data")
const workspaces = JSON.parse(text.trim())
// Initial query returns array directly, event stream wraps it in WorkspacesChanged
handleWorkspacesChanged({ workspaces: workspaces })
} catch (e) {
console.warn("NiriWorkspaceService: Failed to parse initial workspace data:", e)
}
}
}
}
}
function loadInitialWorkspaceData() {
console.log("NiriWorkspaceService: Loading initial workspace data...")
initialDataQuery.running = true
}
// Event stream for real-time updates
Process {
id: eventStreamProcess
command: ["niri", "msg", "-j", "event-stream"]
running: false // Will be enabled after niri check
stdout: SplitParser {
onRead: data => {
try {
const event = JSON.parse(data.trim())
handleNiriEvent(event)
} catch (e) {
console.warn("NiriWorkspaceService: Failed to parse event:", data, e)
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0 && root.niriAvailable) {
console.warn("NiriWorkspaceService: Event stream exited with code", exitCode, "restarting in 2 seconds")
restartTimer.start()
}
}
}
// Restart timer for event stream
Timer {
id: restartTimer
interval: 2000
onTriggered: {
if (root.niriAvailable) {
eventStreamProcess.running = true
}
}
}
function handleNiriEvent(event) {
if (event.WorkspacesChanged) {
handleWorkspacesChanged(event.WorkspacesChanged)
} else if (event.WorkspaceActivated) {
handleWorkspaceActivated(event.WorkspaceActivated)
} else if (event.WindowsChanged) {
handleWindowsChanged(event.WindowsChanged)
} else if (event.WindowClosed) {
handleWindowClosed(event.WindowClosed)
} else if (event.WindowFocusChanged) {
handleWindowFocusChanged(event.WindowFocusChanged)
} else if (event.OverviewOpenedOrClosed) {
handleOverviewChanged(event.OverviewOpenedOrClosed)
}
}
function handleWorkspacesChanged(data) {
allWorkspaces = [...data.workspaces].sort((a, b) => a.idx - b.idx)
// Update focused workspace
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.is_focused)
if (focusedWorkspaceIndex >= 0) {
var focusedWs = allWorkspaces[focusedWorkspaceIndex]
focusedWorkspaceId = focusedWs.id
currentOutput = focusedWs.output || ""
} else {
focusedWorkspaceIndex = 0
focusedWorkspaceId = ""
}
updateCurrentOutputWorkspaces()
}
function handleWorkspaceActivated(data) {
console.log("DEBUG: WorkspaceActivated event - ID:", data.id, "focused:", data.focused)
// Update focused workspace
focusedWorkspaceId = data.id
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.id === data.id)
if (focusedWorkspaceIndex >= 0) {
var activatedWs = allWorkspaces[focusedWorkspaceIndex]
console.log("DEBUG: Found workspace - idx:", activatedWs.idx + 1, "output:", activatedWs.output, "was_active:", activatedWs.is_active)
// Update workspace states properly
// First, deactivate all workspaces on this output
for (var i = 0; i < allWorkspaces.length; i++) {
if (allWorkspaces[i].output === activatedWs.output) {
allWorkspaces[i].is_active = false
allWorkspaces[i].is_focused = false
}
}
// Then activate the new workspace
allWorkspaces[focusedWorkspaceIndex].is_active = true
allWorkspaces[focusedWorkspaceIndex].is_focused = data.focused || false
currentOutput = activatedWs.output || ""
console.log("DEBUG: Activated workspace", activatedWs.idx + 1, "on", currentOutput)
updateCurrentOutputWorkspaces()
} else {
focusedWorkspaceIndex = 0
}
}
function handleWindowsChanged(data) {
windows = [...data.windows].sort((a, b) => a.id - b.id)
updateFocusedWindow()
}
function handleWindowClosed(data) {
windows = windows.filter(w => w.id !== data.id)
updateFocusedWindow()
}
function handleWindowFocusChanged(data) {
if (data.id) {
focusedWindowId = data.id
focusedWindowIndex = windows.findIndex(w => w.id === data.id)
} else {
focusedWindowId = ""
focusedWindowIndex = -1
}
updateFocusedWindow()
}
function handleOverviewChanged(data) {
inOverview = data.is_open
}
function updateCurrentOutputWorkspaces() {
if (!currentOutput) {
currentOutputWorkspaces = allWorkspaces
return
}
// Filter workspaces for current output
var outputWs = allWorkspaces.filter(w => w.output === currentOutput)
currentOutputWorkspaces = outputWs
}
function updateFocusedWindow() {
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) {
var focusedWin = windows[focusedWindowIndex]
focusedWindowTitle = focusedWin.title || "(Unnamed window)"
} else {
focusedWindowTitle = "(No active window)"
}
}
// Public API functions
function switchToWorkspace(workspaceId) {
if (!niriAvailable) return false
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()])
return true
}
function switchToWorkspaceByIndex(index) {
if (!niriAvailable || index < 0 || index >= allWorkspaces.length) return false
var workspace = allWorkspaces[index]
return switchToWorkspace(workspace.id)
}
function switchToWorkspaceByNumber(number) {
if (!niriAvailable) return false
// Find workspace by number (1-based)
var workspace = allWorkspaces.find(w => (w.idx + 1) === number)
if (workspace) {
return switchToWorkspace(workspace.id)
}
// If not found, try to switch by number directly
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", number.toString()])
return true
}
function getWorkspaceByIndex(index) {
if (index >= 0 && index < allWorkspaces.length) {
return allWorkspaces[index]
}
return null
}
function getCurrentOutputWorkspaceNumbers() {
return currentOutputWorkspaces.map(w => w.idx + 1) // niri uses 0-based, UI shows 1-based
}
function getCurrentWorkspaceNumber() {
if (focusedWorkspaceIndex >= 0 && focusedWorkspaceIndex < allWorkspaces.length) {
return allWorkspaces[focusedWorkspaceIndex].idx + 1
}
return 1
}
function isWorkspaceActive(workspaceNumber) {
return workspaceNumber === getCurrentWorkspaceNumber()
}
// For compatibility with existing components
function getCurrentWorkspace() {
return getCurrentWorkspaceNumber()
}
function getWorkspaceList() {
return getCurrentOutputWorkspaceNumbers()
}
}

View File

@@ -11,4 +11,5 @@ singleton BatteryService 1.0 BatteryService.qml
singleton SystemMonitorService 1.0 SystemMonitorService.qml singleton SystemMonitorService 1.0 SystemMonitorService.qml
singleton AppSearchService 1.0 AppSearchService.qml singleton AppSearchService 1.0 AppSearchService.qml
singleton PreferencesService 1.0 PreferencesService.qml singleton PreferencesService 1.0 PreferencesService.qml
singleton LauncherService 1.0 LauncherService.qml singleton LauncherService 1.0 LauncherService.qml
singleton NiriWorkspaceService 1.0 NiriWorkspaceService.qml

View File

@@ -242,100 +242,79 @@ EOF`
radius: Theme.cornerRadiusLarge radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, 0.8) color: Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, 0.8)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: NiriWorkspaceService.niriAvailable
property int currentWorkspace: 1
property var workspaceList: []
Process { // Use reactive properties from NiriWorkspaceService
id: workspaceQuery property int currentWorkspace: getDisplayActiveWorkspace()
command: ["niri", "msg", "workspaces"] property var workspaceList: getDisplayWorkspaces()
running: true
// Get workspaces for this display
function getDisplayWorkspaces() {
// Always return something for now, even if service isn't ready
if (!NiriWorkspaceService.niriAvailable || NiriWorkspaceService.allWorkspaces.length === 0) {
return [1, 2] // Fallback
}
stdout: StdioCollector { if (!topBar.screenName) {
onStreamFinished: { return NiriWorkspaceService.getCurrentOutputWorkspaceNumbers()
if (text && text.trim()) { }
workspaceSwitcher.parseWorkspaceOutput(text.trim())
} // Filter workspaces for this specific display
var displayWorkspaces = []
for (var i = 0; i < NiriWorkspaceService.allWorkspaces.length; i++) {
var ws = NiriWorkspaceService.allWorkspaces[i]
if (ws.output === topBar.screenName) {
displayWorkspaces.push(ws.idx + 1) // Convert to 1-based
} }
} }
return displayWorkspaces.length > 0 ? displayWorkspaces : [1, 2]
} }
function parseWorkspaceOutput(data) { // Get active workspace for this display
const lines = data.split('\n') function getDisplayActiveWorkspace() {
let currentOutputName = "" // Always return something for now, even if service isn't ready
let focusedOutput = "" if (!NiriWorkspaceService.niriAvailable || NiriWorkspaceService.allWorkspaces.length === 0) {
let focusedWorkspace = 1 return 1 // Fallback
let outputWorkspaces = {} }
if (!topBar.screenName) {
return NiriWorkspaceService.getCurrentWorkspaceNumber()
}
for (const line of lines) { // Find active workspace for this display (is_active, not is_focused)
if (line.startsWith('Output "')) { for (var i = 0; i < NiriWorkspaceService.allWorkspaces.length; i++) {
const outputMatch = line.match(/Output "(.+)"/) var ws = NiriWorkspaceService.allWorkspaces[i]
if (outputMatch) { if (ws.output === topBar.screenName && ws.is_active) {
currentOutputName = outputMatch[1] return ws.idx + 1 // Convert to 1-based
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
}
}
} }
} }
// Show workspaces for THIS screen only return 1
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
}
} }
Timer { // React to workspace changes
interval: 500 Connections {
running: true target: NiriWorkspaceService
repeat: true function onAllWorkspacesChanged() {
onTriggered: { var oldCurrent = workspaceSwitcher.currentWorkspace
workspaceQuery.running = true workspaceSwitcher.workspaceList = workspaceSwitcher.getDisplayWorkspaces()
workspaceSwitcher.currentWorkspace = workspaceSwitcher.getDisplayActiveWorkspace()
console.log("DEBUG: TopBar onAllWorkspacesChanged for", topBar.screenName, "- current workspace:", oldCurrent, "→", workspaceSwitcher.currentWorkspace)
}
function onFocusedWorkspaceIndexChanged() {
var oldCurrent = workspaceSwitcher.currentWorkspace
workspaceSwitcher.currentWorkspace = workspaceSwitcher.getDisplayActiveWorkspace()
console.log("DEBUG: TopBar onFocusedWorkspaceIndexChanged for", topBar.screenName, "- current workspace:", oldCurrent, "→", workspaceSwitcher.currentWorkspace)
}
function onNiriAvailableChanged() {
if (NiriWorkspaceService.niriAvailable) {
workspaceSwitcher.workspaceList = workspaceSwitcher.getDisplayWorkspaces()
workspaceSwitcher.currentWorkspace = workspaceSwitcher.getDisplayActiveWorkspace()
}
} }
} }
@@ -351,6 +330,7 @@ EOF`
property bool isActive: modelData === workspaceSwitcher.currentWorkspace property bool isActive: modelData === workspaceSwitcher.currentWorkspace
property bool isHovered: mouseArea.containsMouse property bool isHovered: mouseArea.containsMouse
width: isActive ? Theme.spacingXL + Theme.spacingS : Theme.spacingL width: isActive ? Theme.spacingXL + Theme.spacingS : Theme.spacingL
height: Theme.spacingS height: Theme.spacingS
radius: height / 2 radius: height / 2
@@ -379,44 +359,18 @@ EOF`
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
// Set target workspace and focus monitor first // Use NiriWorkspaceService for workspace switching
console.log("Clicking workspace", modelData, "on monitor", topBar.screenName) if (NiriWorkspaceService.niriAvailable) {
workspaceSwitcher.targetWorkspace = modelData NiriWorkspaceService.switchToWorkspaceByNumber(modelData)
focusMonitorProcess.command = ["niri", "msg", "action", "focus-monitor", topBar.screenName] } else {
focusMonitorProcess.running = true // Fallback for when service isn't ready
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", modelData.toString()])
}
} }
} }
} }
} }
} }
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
} }
} }

View File

@@ -2,120 +2,53 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import "../Common"
import "../Services"
Rectangle { Rectangle {
id: workspaceSwitcher id: workspaceSwitcher
property var theme width: Math.max(120, workspaceRow.implicitWidth + Theme.spacingL * 2)
property var root
width: Math.max(120, workspaceRow.implicitWidth + theme.spacingL * 2)
height: 32 height: 32
radius: theme.cornerRadiusLarge radius: Theme.cornerRadiusLarge
color: Qt.rgba(theme.secondary.r, theme.secondary.g, theme.secondary.b, 0.08) color: Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: NiriWorkspaceService.niriAvailable
property int currentWorkspace: 1 // Use the reactive workspace service
property var workspaceList: [] property int currentWorkspace: NiriWorkspaceService.getCurrentWorkspaceNumber()
property var workspaceList: NiriWorkspaceService.getCurrentOutputWorkspaceNumbers()
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 { Row {
id: workspaceRow id: workspaceRow
anchors.centerIn: parent anchors.centerIn: parent
spacing: theme.spacingS spacing: Theme.spacingS
Repeater { Repeater {
model: workspaceSwitcher.workspaceList model: workspaceSwitcher.workspaceList
Rectangle { Rectangle {
property bool isActive: modelData === workspaceSwitcher.currentWorkspace property bool isActive: NiriWorkspaceService.isWorkspaceActive(modelData)
property bool isHovered: mouseArea.containsMouse property bool isHovered: mouseArea.containsMouse
width: isActive ? theme.spacingXL + theme.spacingS : theme.spacingL width: isActive ? Theme.spacingXL + Theme.spacingS : Theme.spacingL
height: theme.spacingS height: Theme.spacingS
radius: height / 2 radius: height / 2
color: isActive ? theme.primary : color: isActive ? Theme.primary :
isHovered ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.5) : 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) Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
Behavior on width { Behavior on width {
NumberAnimation { NumberAnimation {
duration: theme.mediumDuration duration: Theme.mediumDuration
easing.type: theme.emphasizedEasing easing.type: Theme.emphasizedEasing
} }
} }
Behavior on color { Behavior on color {
ColorAnimation { ColorAnimation {
duration: theme.mediumDuration duration: Theme.mediumDuration
easing.type: theme.emphasizedEasing easing.type: Theme.emphasizedEasing
} }
} }
@@ -126,20 +59,13 @@ Rectangle {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
switchProcess.command = ["niri", "msg", "action", "focus-workspace", modelData.toString()] // Use the service to switch workspaces
switchProcess.running = true // modelData is workspace number (1-based)
workspaceSwitcher.currentWorkspace = modelData NiriWorkspaceService.switchToWorkspaceByNumber(modelData)
Qt.callLater(() => {
workspaceQuery.running = true
})
} }
} }
} }
} }
} }
Process {
id: switchProcess
running: false
}
} }