From 754623c22b485c01bd22956ff314d540cb8ff88e Mon Sep 17 00:00:00 2001 From: David Keijser Date: Thu, 2 Oct 2025 22:28:44 +0200 Subject: [PATCH 1/3] Call compositor backend actions with model reference This allows the compositor backend to decide what property to use when invoking the action --- Modules/Bar/Widgets/Taskbar.qml | 4 ++-- Modules/Bar/Widgets/Workspace.qml | 6 +++--- Services/CompositorService.qml | 12 ++++++------ Services/HyprlandService.qml | 12 ++++++------ Services/NiriService.qml | 12 ++++++------ 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Modules/Bar/Widgets/Taskbar.qml b/Modules/Bar/Widgets/Taskbar.qml index 9a0b356e..a551533c 100644 --- a/Modules/Bar/Widgets/Taskbar.qml +++ b/Modules/Bar/Widgets/Taskbar.qml @@ -105,13 +105,13 @@ Rectangle { if (mouse.button === Qt.LeftButton) { try { - CompositorService.focusWindow(taskbarItem.modelData.id) + CompositorService.focusWindow(taskbarItem.modelData) } catch (error) { Logger.error("Taskbar", "Failed to activate toplevel: " + error) } } else if (mouse.button === Qt.RightButton) { try { - CompositorService.closeWindow(taskbarItem.modelData.id) + CompositorService.closeWindow(taskbarItem.modelData) } catch (error) { Logger.error("Taskbar", "Failed to close toplevel: " + error) } diff --git a/Modules/Bar/Widgets/Workspace.qml b/Modules/Bar/Widgets/Workspace.qml index 689b962f..f093df3e 100644 --- a/Modules/Bar/Widgets/Workspace.qml +++ b/Modules/Bar/Widgets/Workspace.qml @@ -119,7 +119,7 @@ Item { next = localWorkspaces.count - 1 const ws = localWorkspaces.get(next) if (ws && ws.idx !== undefined) - CompositorService.switchToWorkspace(ws.idx) + CompositorService.switchToWorkspace(ws) } Component.onCompleted: { @@ -323,7 +323,7 @@ Item { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { - CompositorService.switchToWorkspace(model.idx) + CompositorService.switchToWorkspace(model) } hoverEnabled: true } @@ -467,7 +467,7 @@ Item { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { - CompositorService.switchToWorkspace(model.idx) + CompositorService.switchToWorkspace(model) } hoverEnabled: true } diff --git a/Services/CompositorService.qml b/Services/CompositorService.qml index 4d30421c..efb55429 100644 --- a/Services/CompositorService.qml +++ b/Services/CompositorService.qml @@ -145,9 +145,9 @@ Singleton { } // Generic workspace switching - function switchToWorkspace(workspaceId) { + function switchToWorkspace(workspace) { if (backend && backend.switchToWorkspace) { - backend.switchToWorkspace(workspaceId) + backend.switchToWorkspace(workspace) } else { Logger.warn("Compositor", "No backend available for workspace switching") } @@ -177,18 +177,18 @@ Singleton { } // Set focused window - function focusWindow(windowId) { + function focusWindow(window) { if (backend && backend.focusWindow) { - backend.focusWindow(windowId) + backend.focusWindow(window) } else { Logger.warn("Compositor", "No backend available for window focus") } } // Close window - function closeWindow(windowId) { + function closeWindow(window) { if (backend && backend.closeWindow) { - backend.closeWindow(windowId) + backend.closeWindow(window) } else { Logger.warn("Compositor", "No backend available for window closing") } diff --git a/Services/HyprlandService.qml b/Services/HyprlandService.qml index c27beba1..81d09549 100644 --- a/Services/HyprlandService.qml +++ b/Services/HyprlandService.qml @@ -272,25 +272,25 @@ Item { } // Public functions - function switchToWorkspace(workspaceId) { + function switchToWorkspace(workspace) { try { - Hyprland.dispatch(`workspace ${workspaceId}`) + Hyprland.dispatch(`workspace ${workspace.idx}`) } catch (e) { Logger.error("HyprlandService", "Failed to switch workspace:", e) } } - function focusWindow(windowId) { + function focusWindow(window) { try { - Hyprland.dispatch(`focuswindow address:0x${windowId.toString()}`) + Hyprland.dispatch(`focuswindow address:0x${window.id.toString()}`) } catch (e) { Logger.error("HyprlandService", "Failed to switch window:", e) } } - function closeWindow(windowId) { + function closeWindow(window) { try { - Hyprland.dispatch(`killwindow address:0x${windowId}`) + Hyprland.dispatch(`killwindow address:0x${window.id}`) } catch (e) { Logger.error("HyprlandService", "Failed to close window:", e) } diff --git a/Services/NiriService.qml b/Services/NiriService.qml index 0c2d26f0..9f3df88b 100644 --- a/Services/NiriService.qml +++ b/Services/NiriService.qml @@ -332,25 +332,25 @@ Item { } // Public functions - function switchToWorkspace(workspaceId) { + function switchToWorkspace(workspace) { try { - Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]) + Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspace.idx.toString()]) } catch (e) { Logger.error("NiriService", "Failed to switch workspace:", e) } } - function focusWindow(windowId) { + function focusWindow(window) { try { - Quickshell.execDetached(["niri", "msg", "action", "focus-window", "--id", windowId.toString()]) + Quickshell.execDetached(["niri", "msg", "action", "focus-window", "--id", window.id.toString()]) } catch (e) { Logger.error("NiriService", "Failed to switch window:", e) } } - function closeWindow(windowId) { + function closeWindow(window) { try { - Quickshell.execDetached(["niri", "msg", "action", "close-window", "--id", windowId.toString()]) + Quickshell.execDetached(["niri", "msg", "action", "close-window", "--id", window.id.toString()]) } catch (e) { Logger.error("NiriService", "Failed to close window:", e) } From 31a64abcaaf1a07e94c4ee0fae074044dee982ff Mon Sep 17 00:00:00 2001 From: David Keijser Date: Thu, 2 Oct 2025 22:31:25 +0200 Subject: [PATCH 2/3] Create SwayService This is for the most part a copy-paste job of hyprland. Uses ToplevelManager to provide information about toplevels which is not available from the I3 api. Some features like taskbar focus is a bit broken as this happens by app_id which falls apart for XWayland windows and applications with multiple open windows. --- Services/CompositorService.qml | 17 +++ Services/SwayService.qml | 233 +++++++++++++++++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 Services/SwayService.qml diff --git a/Services/CompositorService.qml b/Services/CompositorService.qml index efb55429..fe81878a 100644 --- a/Services/CompositorService.qml +++ b/Services/CompositorService.qml @@ -11,6 +11,7 @@ Singleton { // Compositor detection property bool isHyprland: false property bool isNiri: false + property bool isSway: false // Generic workspace and window data property ListModel workspaces: ListModel {} @@ -31,14 +32,22 @@ Singleton { function detectCompositor() { const hyprlandSignature = Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE") + const swaySock = Quickshell.env("SWAYSOCK") if (hyprlandSignature && hyprlandSignature.length > 0) { isHyprland = true isNiri = false + isSway = false backendLoader.sourceComponent = hyprlandComponent + } else if (swaySock && swaySock.length > 0) { + isHyprland = false + isNiri = false + isSway = true + backendLoader.sourceComponent = swayComponent } else { // Default to Niri isHyprland = false isNiri = true + isSway = false backendLoader.sourceComponent = niriComponent } } @@ -70,6 +79,14 @@ Singleton { } } + // Sway backend component + Component { + id: swayComponent + SwayService { + id: swayBackend + } + } + function setupBackendConnections() { if (!backend) return diff --git a/Services/SwayService.qml b/Services/SwayService.qml new file mode 100644 index 00000000..62e176d6 --- /dev/null +++ b/Services/SwayService.qml @@ -0,0 +1,233 @@ +import QtQuick +import Quickshell +import Quickshell.I3 +import Quickshell.Wayland +import qs.Commons + +Item { + id: root + + // Properties that match the facade interface + property ListModel workspaces: ListModel {} + property var windows: [] + property int focusedWindowIndex: -1 + + // Signals that match the facade interface + signal workspaceChanged + signal activeWindowChanged + signal windowListChanged + + // I3-specific properties + property bool initialized: false + + // Debounce timer for updates + Timer { + id: updateTimer + interval: 50 + repeat: false + onTriggered: safeUpdate() + } + + // Initialization + function initialize() { + if (initialized) + return + + try { + I3.refreshWorkspaces() + Qt.callLater(() => { + safeUpdateWorkspaces() + safeUpdateWindows() + }) + initialized = true + Logger.log("SwayService", "Initialized successfully") + } catch (e) { + Logger.error("SwayService", "Failed to initialize:", e) + } + } + + // Safe update wrapper + function safeUpdate() { + safeUpdateWindows() + safeUpdateWorkspaces() + windowListChanged() + } + + // Safe workspace update + function safeUpdateWorkspaces() { + try { + workspaces.clear() + + if (!I3.workspaces || !I3.workspaces.values) { + return + } + + const hlWorkspaces = I3.workspaces.values + + for (var i = 0; i < hlWorkspaces.length; i++) { + const ws = hlWorkspaces[i] + if (!ws || ws.id < 1) + continue + + const wsData = { + "id": i, + "idx": ws.id, + "name": ws.name || "", + "output": (ws.monitor && ws.monitor.name) ? ws.monitor.name : "", + "isActive": ws.active === true, + "isFocused": ws.focused === true, + "isUrgent": ws.urgent === true, + "isOccupied": true + } + + workspaces.append(wsData) + } + } catch (e) { + Logger.error("SwayService", "Error updating workspaces:", e) + } + } + + // Safe window update + function safeUpdateWindows() { + try { + const windowsList = [] + + if (!ToplevelManager.toplevels || !ToplevelManager.toplevels.values) { + windows = [] + focusedWindowIndex = -1 + return + } + + const hlToplevels = ToplevelManager.toplevels.values + let newFocusedIndex = -1 + + for (var i = 0; i < hlToplevels.length; i++) { + const toplevel = hlToplevels[i] + if (!toplevel) + continue + + const windowData = extractWindowData(toplevel) + if (windowData) { + windowsList.push(windowData) + + if (windowData.isFocused) { + newFocusedIndex = windowsList.length - 1 + } + } + } + + windows = windowsList + + if (newFocusedIndex !== focusedWindowIndex) { + focusedWindowIndex = newFocusedIndex + activeWindowChanged() + } + } catch (e) { + Logger.error("SwayService", "Error updating windows:", e) + } + } + + // Extract window data safely from a toplevel + function extractWindowData(toplevel) { + if (!toplevel) + return null + + try { + // Safely extract properties + const appId = extractAppId(toplevel) + const title = safeGetProperty(toplevel, "title", "") + const focused = toplevel.activated === true + + return { + "title": title, + "appId": appId, + "isFocused": focused + } + } catch (e) { + return null + } + } + + // Extract app ID from various possible sources + function extractAppId(toplevel) { + if (!toplevel) + return "" + + return toplevel.appId; + } + + // Safe property getter + function safeGetProperty(obj, prop, defaultValue) { + try { + const value = obj[prop] + if (value !== undefined && value !== null) { + return String(value) + } + } catch (e) { + + // Property access failed + } + return defaultValue + } + + // Connections to I3 + Connections { + target: I3.workspaces + enabled: initialized + function onValuesChanged() { + safeUpdateWorkspaces() + workspaceChanged() + } + } + + Connections { + target: ToplevelManager + enabled: initialized + function onActiveToplevelChanged() { + updateTimer.restart() + } + } + + Connections { + target: I3 + enabled: initialized + function onRawEvent(event) { + safeUpdateWorkspaces() + workspaceChanged() + updateTimer.restart() + } + } + + // Public functions + function switchToWorkspace(workspace) { + try { + I3.dispatch(`workspace ${workspace.name}`) + } catch (e) { + Logger.error("SwayService", "Failed to switch workspace:", e) + } + } + + function focusWindow(window) { + try { + I3.dispatch(`[app_id="${window.appId}"] focus`) + } catch (e) { + Logger.error("SwayService", "Failed to switch window:", e) + } + } + + function closeWindow(window) { + try { + I3.dispatch(`[app_id="${window.appId}"] kill`) + } catch (e) { + Logger.error("SwayService", "Failed to close window:", e) + } + } + + function logout() { + try { + Quickshell.execDetached(["swaymsg", "exit"]) + } catch (e) { + Logger.error("SwayService", "Failed to logout:", e) + } + } +} From 879d428e7d80bbf120fa276a2c9059c48f11d9ad Mon Sep 17 00:00:00 2001 From: David Keijser Date: Fri, 3 Oct 2025 17:23:06 +0200 Subject: [PATCH 3/3] Use wayland handle to toplevel/workspace to activate on sway This avoids the issue with app_id filter where multiple windows of the same app is present. --- Services/SwayService.qml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Services/SwayService.qml b/Services/SwayService.qml index 62e176d6..fbcb2d45 100644 --- a/Services/SwayService.qml +++ b/Services/SwayService.qml @@ -77,7 +77,8 @@ Item { "isActive": ws.active === true, "isFocused": ws.focused === true, "isUrgent": ws.urgent === true, - "isOccupied": true + "isOccupied": true, + "handle": ws } workspaces.append(wsData) @@ -141,7 +142,8 @@ Item { return { "title": title, "appId": appId, - "isFocused": focused + "isFocused": focused, + "handle": toplevel } } catch (e) { return null @@ -201,7 +203,7 @@ Item { // Public functions function switchToWorkspace(workspace) { try { - I3.dispatch(`workspace ${workspace.name}`) + workspace.handle.activate() } catch (e) { Logger.error("SwayService", "Failed to switch workspace:", e) } @@ -209,7 +211,7 @@ Item { function focusWindow(window) { try { - I3.dispatch(`[app_id="${window.appId}"] focus`) + window.handle.activate() } catch (e) { Logger.error("SwayService", "Failed to switch window:", e) } @@ -217,7 +219,7 @@ Item { function closeWindow(window) { try { - I3.dispatch(`[app_id="${window.appId}"] kill`) + window.handle.close() } catch (e) { Logger.error("SwayService", "Failed to close window:", e) }