Merge pull request #411 from keis/sway

Basic sway support
This commit is contained in:
Lemmy
2025-10-07 20:56:06 -04:00
committed by GitHub
6 changed files with 275 additions and 23 deletions

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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
@@ -145,9 +162,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 +194,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")
}

View File

@@ -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)
}

View File

@@ -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)
}

235
Services/SwayService.qml Normal file
View File

@@ -0,0 +1,235 @@
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,
"handle": ws
}
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,
"handle": toplevel
}
} 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 {
workspace.handle.activate()
} catch (e) {
Logger.error("SwayService", "Failed to switch workspace:", e)
}
}
function focusWindow(window) {
try {
window.handle.activate()
} catch (e) {
Logger.error("SwayService", "Failed to switch window:", e)
}
}
function closeWindow(window) {
try {
window.handle.close()
} 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)
}
}
}