1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-05 21:15:38 -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 AppSearchService 1.0 AppSearchService.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
color: Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, 0.8)
anchors.verticalCenter: parent.verticalCenter
visible: NiriWorkspaceService.niriAvailable
property int currentWorkspace: 1
property var workspaceList: []
Process {
id: workspaceQuery
command: ["niri", "msg", "workspaces"]
running: true
// Use reactive properties from NiriWorkspaceService
property int currentWorkspace: getDisplayActiveWorkspace()
property var workspaceList: getDisplayWorkspaces()
// 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 {
onStreamFinished: {
if (text && text.trim()) {
workspaceSwitcher.parseWorkspaceOutput(text.trim())
}
if (!topBar.screenName) {
return NiriWorkspaceService.getCurrentOutputWorkspaceNumbers()
}
// 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) {
const lines = data.split('\n')
let currentOutputName = ""
let focusedOutput = ""
let focusedWorkspace = 1
let outputWorkspaces = {}
// Get active workspace for this display
function getDisplayActiveWorkspace() {
// Always return something for now, even if service isn't ready
if (!NiriWorkspaceService.niriAvailable || NiriWorkspaceService.allWorkspaces.length === 0) {
return 1 // Fallback
}
if (!topBar.screenName) {
return NiriWorkspaceService.getCurrentWorkspaceNumber()
}
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
}
}
// Find active workspace for this display (is_active, not is_focused)
for (var i = 0; i < NiriWorkspaceService.allWorkspaces.length; i++) {
var ws = NiriWorkspaceService.allWorkspaces[i]
if (ws.output === topBar.screenName && ws.is_active) {
return ws.idx + 1 // Convert to 1-based
}
}
// 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
}
return 1
}
Timer {
interval: 500
running: true
repeat: true
onTriggered: {
workspaceQuery.running = true
// React to workspace changes
Connections {
target: NiriWorkspaceService
function onAllWorkspacesChanged() {
var oldCurrent = workspaceSwitcher.currentWorkspace
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 isHovered: mouseArea.containsMouse
width: isActive ? Theme.spacingXL + Theme.spacingS : Theme.spacingL
height: Theme.spacingS
radius: height / 2
@@ -379,44 +359,18 @@ EOF`
cursorShape: Qt.PointingHandCursor
onClicked: {
// 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
// Use NiriWorkspaceService for workspace switching
if (NiriWorkspaceService.niriAvailable) {
NiriWorkspaceService.switchToWorkspaceByNumber(modelData)
} else {
// 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 Quickshell
import Quickshell.Io
import "../Common"
import "../Services"
Rectangle {
id: workspaceSwitcher
property var theme
property var root
width: Math.max(120, workspaceRow.implicitWidth + theme.spacingL * 2)
width: Math.max(120, workspaceRow.implicitWidth + Theme.spacingL * 2)
height: 32
radius: theme.cornerRadiusLarge
color: Qt.rgba(theme.secondary.r, theme.secondary.g, theme.secondary.b, 0.08)
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
anchors.verticalCenter: parent.verticalCenter
visible: NiriWorkspaceService.niriAvailable
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
}
}
// Use the reactive workspace service
property int currentWorkspace: NiriWorkspaceService.getCurrentWorkspaceNumber()
property var workspaceList: NiriWorkspaceService.getCurrentOutputWorkspaceNumbers()
Row {
id: workspaceRow
anchors.centerIn: parent
spacing: theme.spacingS
spacing: Theme.spacingS
Repeater {
model: workspaceSwitcher.workspaceList
Rectangle {
property bool isActive: modelData === workspaceSwitcher.currentWorkspace
property bool isActive: NiriWorkspaceService.isWorkspaceActive(modelData)
property bool isHovered: mouseArea.containsMouse
width: isActive ? theme.spacingXL + theme.spacingS : theme.spacingL
height: theme.spacingS
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)
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
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on color {
ColorAnimation {
duration: theme.mediumDuration
easing.type: theme.emphasizedEasing
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
@@ -126,20 +59,13 @@ Rectangle {
cursorShape: Qt.PointingHandCursor
onClicked: {
switchProcess.command = ["niri", "msg", "action", "focus-workspace", modelData.toString()]
switchProcess.running = true
workspaceSwitcher.currentWorkspace = modelData
Qt.callLater(() => {
workspaceQuery.running = true
})
// Use the service to switch workspaces
// modelData is workspace number (1-based)
NiriWorkspaceService.switchToWorkspaceByNumber(modelData)
}
}
}
}
}
Process {
id: switchProcess
running: false
}
}