diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index c1904459..7a296e82 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -30,6 +30,9 @@ Item { return {} } + readonly property string windowTitle: CompositorService.getFocusedWindowTitle() + + readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : widgetMetadata.showIcon // 6% of total width @@ -40,26 +43,19 @@ Item { readonly property bool isVertical: barPosition === "left" || barPosition === "right" readonly property bool compact: (Settings.data.bar.density === "compact") - implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling) - implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * 0.8 * scaling) : (horizontalLayout.implicitWidth + Style.marginM * 2 * scaling) - - readonly property real textSize: { + readonly property real textSize: { var base = isVertical ? width : height return Math.max(1, compact ? base * 0.43 : base * 0.33) } readonly property real iconSize: textSize * 1.25 - function getTitle() { - try { - return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : "" - } catch (e) { - Logger.warn("ActiveWindow", "Error getting title:", e) - return "" - } - } + implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling) + implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * 0.8 * scaling) : (horizontalLayout.implicitWidth + Style.marginM * 2 * scaling) - visible: getTitle() !== "" + + + visible: windowTitle !== "" function calculatedVerticalHeight() { // Use standard widget height like other widgets @@ -74,11 +70,10 @@ Item { } // Calculate actual text width more accurately - const title = getTitle() - if (title !== "") { + if (windowTitle !== "") { // Estimate text width: average character width * number of characters const avgCharWidth = Style.fontSizeS * scaling * 0.6 // rough estimate - const titleWidth = Math.min(title.length * avgCharWidth, 80 * scaling) + const titleWidth = Math.min(windowTitle.length * avgCharWidth, 80 * scaling) total += titleWidth } @@ -131,7 +126,7 @@ Item { NText { id: fullTitleMetrics visible: false - text: getTitle() + text: windowTitle font.pointSize: Style.fontSizeS * scaling font.weight: Style.fontWeightMedium } @@ -167,7 +162,7 @@ Item { Layout.preferredWidth: Style.capsuleHeight * 0.75 * scaling Layout.preferredHeight: Style.capsuleHeight * 0.75 * scaling Layout.alignment: Qt.AlignVCenter - visible: getTitle() !== "" && showIcon + visible: windowTitle !== "" && showIcon IconImage { id: windowIcon @@ -202,7 +197,7 @@ Item { } Layout.alignment: Qt.AlignVCenter horizontalAlignment: Text.AlignLeft - text: getTitle() + text: windowTitle font.pointSize: Style.fontSizeS * scaling font.weight: Style.fontWeightMedium elide: mouseArea.containsMouse ? Text.ElideNone : Text.ElideRight @@ -273,7 +268,7 @@ Item { NTooltip { id: tooltip target: verticalLayout - text: getTitle() + text: windowTitle positionLeft: barPosition === "right" positionRight: barPosition === "left" delay: 500 diff --git a/Modules/Bar/Widgets/Workspace.qml b/Modules/Bar/Widgets/Workspace.qml index 1a4f30e1..ce8b0ae8 100644 --- a/Modules/Bar/Widgets/Workspace.qml +++ b/Modules/Bar/Widgets/Workspace.qml @@ -111,7 +111,7 @@ Item { onHideUnoccupiedChanged: refreshWorkspaces() Connections { - target: WorkspaceService + target: CompositorService function onWorkspacesChanged() { refreshWorkspaces() } @@ -120,8 +120,8 @@ Item { function refreshWorkspaces() { localWorkspaces.clear() if (screen !== null) { - for (var i = 0; i < WorkspaceService.workspaces.count; i++) { - const ws = WorkspaceService.workspaces.get(i) + for (var i = 0; i < CompositorService.workspaces.count; i++) { + const ws = CompositorService.workspaces.get(i) if (ws.output.toLowerCase() === screen.name.toLowerCase()) { if (hideUnoccupied && !ws.isOccupied && !ws.isFocused) { continue @@ -260,7 +260,7 @@ Item { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { - WorkspaceService.switchToWorkspace(model.idx) + CompositorService.switchToWorkspace(model.idx) } hoverEnabled: true } @@ -404,7 +404,7 @@ Item { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { - WorkspaceService.switchToWorkspace(model.idx) + CompositorService.switchToWorkspace(model.idx) } hoverEnabled: true } diff --git a/Services/CompositorService.qml b/Services/CompositorService.qml index 615cb824..0f0e41cd 100644 --- a/Services/CompositorService.qml +++ b/Services/CompositorService.qml @@ -2,616 +2,128 @@ pragma Singleton import QtQuick import Quickshell -import Quickshell.Io -import Quickshell.Hyprland import qs.Commons import qs.Services Singleton { id: root - // Generic compositor properties - property string compositorType: "unknown" // "hyprland", "niri", or "unknown" + // Compositor detection property bool isHyprland: false property bool isNiri: false - readonly property string hyprlandSignature: Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE") - // Generic workspace and window data property ListModel workspaces: ListModel {} property var windows: [] property int focusedWindowIndex: -1 - property string focusedWindowTitle: "n/a" - property bool inOverview: false // Generic events signal workspaceChanged signal activeWindowChanged - signal overviewStateChanged signal windowListChanged - signal windowTitleChanged - // Debounce timer for updates - property Timer updateTimer: Timer { - interval: 50 // 50ms debounce - repeat: false - onTriggered: { - try { - updateHyprlandWindows() - updateHyprlandWorkspaces() - windowListChanged() - } catch (e) { - Logger.error("Compositor", "Error in debounced update:", e) - } - } - } + // Backend service loader + property var backend: null - // Compositor detection Component.onCompleted: { detectCompositor() } - // Hyprland connections - Loader { - active: isHyprland - sourceComponent: Component { - Item { - Connections { - target: Hyprland.workspaces - enabled: isHyprland - function onValuesChanged() { - try { - updateHyprlandWorkspaces() - workspaceChanged() - } catch (e) { - Logger.error("Compositor", "Error in workspaces onValuesChanged:", e) - } - } - } - - Connections { - target: Hyprland.toplevels - enabled: isHyprland - function onValuesChanged() { - try { - // Use debounced update to prevent too frequent calls - updateTimer.restart() - } catch (e) { - Logger.error("Compositor", "Error in toplevels onValuesChanged:", e) - } - } - } - - Connections { - target: Hyprland - enabled: isHyprland - function onRawEvent(event) { - try { - updateHyprlandWorkspaces() - workspaceChanged() - updateTimer.restart() - } catch (e) { - Logger.error("Compositor", "Error in rawEvent:", e) - } - } - } - } - } - } - function detectCompositor() { - try { - // Try Hyprland first - if (hyprlandSignature && hyprlandSignature.length > 0) { - compositorType = "hyprland" - isHyprland = true - isNiri = false - initHyprland() - return - } - } catch (e) { - - // Hyprland not available - } - - // Try Niri (always available since we handle it directly) - compositorType = "niri" - isHyprland = false - isNiri = true - initNiri() - return - - // No supported compositor found - compositorType = "unknown" - isHyprland = false - isNiri = false - Logger.warn("Compositor", "No supported compositor detected") - } - - // Hyprland integration - function initHyprland() { - try { - Hyprland.refreshWorkspaces() - Hyprland.refreshToplevels() - updateHyprlandWorkspaces() - updateHyprlandWindows() - setupHyprlandConnections() - Logger.log("Compositor", "Hyprland initialized successfully") - } catch (e) { - Logger.error("Compositor", "Error initializing Hyprland:", e) - compositorType = "unknown" - isHyprland = false - } - } - - function setupHyprlandConnections() {// Connections are set up at the top level, this function just marks that Hyprland is ready - } - - function updateHyprlandWorkspaces() { - if (!isHyprland) - return - - workspaces.clear() - try { - const hlWorkspaces = Hyprland.workspaces.values - - // Determine occupied workspace ids from current toplevels - const occupiedIds = {} - try { - const hlToplevels = Hyprland.toplevels.values - for (var t = 0; t < hlToplevels.length; t++) { - const toplevel = hlToplevels[t] - if (toplevel) { - try { - const tws = toplevel.workspace?.id - if (tws !== undefined && tws !== null) { - occupiedIds[tws] = true - } - } catch (toplevelError) { - // Ignore errors from individual toplevels - continue - } - } - } - } catch (e2) { - - // ignore occupancy errors; fall back to false - } - - for (var i = 0; i < hlWorkspaces.length; i++) { - const ws = hlWorkspaces[i] - if (!ws) - continue - - try { - // Only append workspaces with id >= 1 - if (ws.id >= 1) { - workspaces.append({ - "id": i, - "idx": ws.id, - "name": ws.name || "", - "output": ws.monitor?.name || "", - "isActive": ws.active === true, - "isFocused": ws.focused === true, - "isUrgent": ws.urgent === true, - "isOccupied": occupiedIds[ws.id] === true - }) - } - } catch (workspaceError) { - Logger.warn("Compositor", "Error processing workspace at index", i, ":", workspaceError) - continue - } - } - } catch (e) { - Logger.error("Compositor", "Error updating Hyprland workspaces:", e) - } - } - - function updateHyprlandWindows() { - if (!isHyprland) - return - - try { - const hlToplevels = Hyprland.toplevels.values - const windowsList = [] - - for (var i = 0; i < hlToplevels.length; i++) { - const toplevel = hlToplevels[i] - - // Skip if toplevel is null or invalid - if (!toplevel) { - continue - } - - try { - // Try to get appId from various sources with proper null checks - let appId = "" - - // First try the direct properties with null/undefined checks - try { - if (toplevel.class !== undefined && toplevel.class !== null) { - appId = String(toplevel.class) - } else if (toplevel.initialClass !== undefined && toplevel.initialClass !== null) { - appId = String(toplevel.initialClass) - } else if (toplevel.appId !== undefined && toplevel.appId !== null) { - appId = String(toplevel.appId) - } - } catch (propertyError) { - - // Ignore property access errors and continue with empty appId - } - - // If still no appId, try to get it from the lastIpcObject - if (!appId) { - try { - const ipcData = toplevel.lastIpcObject - if (ipcData) { - appId = String(ipcData.class || ipcData.initialClass || ipcData.appId || ipcData.wm_class || "") - } - } catch (ipcError) { - - // Ignore errors when accessing lastIpcObject - } - } - - // Safely get other properties with fallbacks - let windowId = "" - let windowTitle = "" - let workspaceId = null - let isActivated = false - - try { - windowId = (toplevel.address !== undefined && toplevel.address !== null) ? String(toplevel.address) : "" - } catch (e) { - windowId = "" - } - - try { - windowTitle = (toplevel.title !== undefined && toplevel.title !== null) ? String(toplevel.title) : "" - } catch (e) { - windowTitle = "" - } - - try { - workspaceId = toplevel.workspace?.id || null - } catch (e) { - workspaceId = null - } - - try { - isActivated = toplevel.activated === true - } catch (e) { - isActivated = false - } - - windowsList.push({ - "id": windowId, - "title": windowTitle, - "appId": appId, - "workspaceId": workspaceId, - "isFocused": isActivated - }) - } catch (toplevelError) { - // Log the error but continue processing other toplevels - Logger.warn("Compositor", "Error processing toplevel at index", i, ":", toplevelError) - continue - } - } - - windows = windowsList - - // Update focused window index - focusedWindowIndex = -1 - for (var j = 0; j < windowsList.length; j++) { - if (windowsList[j].isFocused) { - focusedWindowIndex = j - break - } - } - - updateFocusedWindowTitle() - activeWindowChanged() - } catch (e) { - Logger.error("Compositor", "Error updating Hyprland windows:", e) - // Don't crash, just keep the previous windows list - } - } - - // Niri integration - function initNiri() { - try { - // Start the event stream to receive Niri events - niriEventStream.running = true - // Initial load of workspaces and windows - updateNiriWorkspaces() - updateNiriWindows() - Logger.log("Compositor", "Niri initialized successfully") - } catch (e) { - Logger.error("Compositor", "Error initializing Niri:", e) - compositorType = "unknown" + const hyprlandSignature = Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE") + if (hyprlandSignature && hyprlandSignature.length > 0) { + isHyprland = true isNiri = false - } - } - - function updateNiriWorkspaces() { - if (!isNiri) - return - - // Get workspaces from the Niri process - niriWorkspaceProcess.running = true - } - - function updateNiriWindows() { - if (!isNiri) - return - - // Get windows from the Niri process - niriWindowsProcess.running = true - } - - // Niri workspace process - Process { - id: niriWorkspaceProcess - running: false - command: ["niri", "msg", "--json", "workspaces"] - - stdout: SplitParser { - onRead: function (line) { - try { - const workspacesData = JSON.parse(line) - const workspacesList = [] - - for (const ws of workspacesData) { - workspacesList.push({ - "id": ws.id, - "idx": ws.idx, - "name": ws.name || "", - "output": ws.output || "", - "isFocused": ws.is_focused === true, - "isActive": ws.is_active === true, - "isUrgent": ws.is_urgent === true, - "isOccupied": ws.active_window_id ? true : false - }) - } - - workspacesList.sort((a, b) => { - if (a.output !== b.output) { - return a.output.localeCompare(b.output) - } - return a.idx - b.idx - }) - - // Update the workspaces ListModel - workspaces.clear() - for (var i = 0; i < workspacesList.length; i++) { - workspaces.append(workspacesList[i]) - } - workspaceChanged() - } catch (e) { - Logger.error("Compositor", "Failed to parse workspaces:", e, line) - } - } - } - } - - // Niri event stream process - Process { - id: niriEventStream - running: false - command: ["niri", "msg", "--json", "event-stream"] - - stdout: SplitParser { - onRead: data => { - try { - const event = JSON.parse(data.trim()) - - if (event.WorkspacesChanged) { - niriWorkspaceProcess.running = true - } else if (event.WindowOpenedOrChanged) { - try { - const windowData = event.WindowOpenedOrChanged.window - - // Find if this window already exists - const existingIndex = windows.findIndex(w => w.id === windowData.id) - - const newWindow = { - "id": windowData.id, - "title": windowData.title || "", - "appId": windowData.app_id || "", - "workspaceId": windowData.workspace_id || null, - "isFocused": windowData.is_focused === true - } - - if (existingIndex >= 0) { - // Update existing window - windows[existingIndex] = newWindow - } else { - // Add new window - windows.push(newWindow) - windows.sort((a, b) => a.id - b.id) - } - - // Update focused window index if this window is focused - if (newWindow.isFocused) { - const oldFocusedIndex = focusedWindowIndex - focusedWindowIndex = windows.findIndex(w => w.id === windowData.id) - updateFocusedWindowTitle() - - // Only emit activeWindowChanged if the focused window actually changed - if (oldFocusedIndex !== focusedWindowIndex) { - activeWindowChanged() - } - } else if (existingIndex >= 0 && existingIndex === focusedWindowIndex) { - // If this is the currently focused window (but not newly focused), - // still update the title in case it changed, but don't emit activeWindowChanged - updateFocusedWindowTitle() - } - - windowListChanged() - } catch (e) { - Logger.error("Compositor", "Error parsing WindowOpenedOrChanged event:", e) - } - } else if (event.WindowClosed) { - try { - const windowId = event.WindowClosed.id - - // Remove the window from the list - const windowIndex = windows.findIndex(w => w.id === windowId) - if (windowIndex >= 0) { - // If this was the focused window, clear focus - if (windowIndex === focusedWindowIndex) { - focusedWindowIndex = -1 - updateFocusedWindowTitle() - activeWindowChanged() - } - - // Remove the window - windows.splice(windowIndex, 1) - - // Adjust focused window index if needed - if (focusedWindowIndex > windowIndex) { - focusedWindowIndex-- - } - - windowListChanged() - } - } catch (e) { - Logger.error("Compositor", "Error parsing WindowClosed event:", e) - } - } else if (event.WindowsChanged) { - try { - const windowsData = event.WindowsChanged.windows - const windowsList = [] - for (const win of windowsData) { - windowsList.push({ - "id": win.id, - "title": win.title || "", - "appId": win.app_id || "", - "workspaceId": win.workspace_id || null, - "isFocused": win.is_focused === true - }) - } - - windowsList.sort((a, b) => a.id - b.id) - windows = windowsList - windowListChanged() - - // Update focused window index - for (var i = 0; i < windowsList.length; i++) { - if (windowsList[i].isFocused) { - focusedWindowIndex = i - break - } - } - updateFocusedWindowTitle() - activeWindowChanged() - } catch (e) { - Logger.error("Compositor", "Error parsing windows event:", e) - } - } else if (event.WorkspaceActivated) { - niriWorkspaceProcess.running = true - } else if (event.WindowFocusChanged) { - try { - const focusedId = event.WindowFocusChanged.id - if (focusedId) { - focusedWindowIndex = windows.findIndex(w => w.id === focusedId) - if (focusedWindowIndex < 0) { - focusedWindowIndex = 0 - } - } else { - focusedWindowIndex = -1 - } - updateFocusedWindowTitle() - activeWindowChanged() - } catch (e) { - Logger.error("Compositor", "Error parsing window focus event:", e) - } - } else if (event.OverviewOpenedOrClosed) { - try { - inOverview = event.OverviewOpenedOrClosed.is_open === true - overviewStateChanged() - } catch (e) { - Logger.error("Compositor", "Error parsing overview state:", e) - } - } - } catch (e) { - Logger.error("Compositor", "Error parsing event stream:", e, data) - } - } - } - } - - // Niri windows process (for initial load) - Process { - id: niriWindowsProcess - running: false - command: ["niri", "msg", "--json", "windows"] - - stdout: SplitParser { - onRead: function (line) { - try { - const windowsData = JSON.parse(line) - const windowsList = [] - for (const win of windowsData) { - windowsList.push({ - "id": win.id, - "title": win.title || "", - "appId": win.app_id || "", - "workspaceId": win.workspace_id || null, - "isFocused": win.is_focused === true - }) - } - - windowsList.sort((a, b) => a.id - b.id) - windows = windowsList - windowListChanged() - - // Update focused window index - for (var i = 0; i < windowsList.length; i++) { - if (windowsList[i].isFocused) { - focusedWindowIndex = i - break - } - } - updateFocusedWindowTitle() - activeWindowChanged() - } catch (e) { - Logger.error("Compositor", "Failed to parse windows:", e, line) - } - } - } - } - - function updateFocusedWindowTitle() { - const oldTitle = focusedWindowTitle - if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) { - focusedWindowTitle = windows[focusedWindowIndex].title || "(Unnamed window)" + backendLoader.sourceComponent = hyprlandComponent } else { - focusedWindowTitle = "(No active window)" + // Default to Niri + isHyprland = false + isNiri = true + backendLoader.sourceComponent = niriComponent } + } - // Emit signal if title actually changed - if (oldTitle !== focusedWindowTitle) { - windowTitleChanged() + Loader { + id: backendLoader + onLoaded: { + if (item) { + root.backend = item + setupBackendConnections() + backend.initialize() + } } } + // Hyprland backend component + Component { + id: hyprlandComponent + HyprlandService { + id: hyprlandBackend + } + } + + // Niri backend component + Component { + id: niriComponent + NiriService { + id: niriBackend + } + } + + function setupBackendConnections() { + if (!backend) return + + // Connect backend signals to facade signals + backend.workspaceChanged.connect(() => { + // Sync workspaces when they change + syncWorkspaces() + // Forward the signal + workspaceChanged() + }) + + backend.activeWindowChanged.connect(activeWindowChanged) + backend.windowListChanged.connect(() => { + // Sync windows when they change + windows = backend.windows + // Forward the signal + windowListChanged() + }) + + // Property bindings + backend.focusedWindowIndexChanged.connect(() => { + focusedWindowIndex = backend.focusedWindowIndex + }) + + // Initial sync + syncWorkspaces() + windows = backend.windows + focusedWindowIndex = backend.focusedWindowIndex + } + + function syncWorkspaces() { + workspaces.clear() + const ws = backend.workspaces + for (var i = 0; i < ws.count; i++) { + workspaces.append(ws.get(i)) + } + // Emit signal to notify listeners that workspace list has been updated + workspacesChanged() + } + + // Get window title for focused window + function getFocusedWindowTitle() { + if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) { + return windows[focusedWindowIndex].title || "(Unnamed window)" + } + return "(No active window)" + } + // Generic workspace switching function switchToWorkspace(workspaceId) { - if (isHyprland) { - try { - Hyprland.dispatch(`workspace ${workspaceId}`) - } catch (e) { - Logger.error("Compositor", "Error switching Hyprland workspace:", e) - } - } else if (isNiri) { - try { - Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]) - } catch (e) { - Logger.error("Compositor", "Error switching Niri workspace:", e) - } + if (backend && backend.switchToWorkspace) { + backend.switchToWorkspace(workspaceId) } else { - Logger.warn("Compositor", "No supported compositor detected for workspace switching") + Logger.warn("Compositor", "No backend available for workspace switching") } } @@ -634,22 +146,12 @@ Singleton { return null } - // Generic logout/shutdown commands + // Session management function logout() { - if (isHyprland) { - try { - Quickshell.execDetached(["hyprctl", "dispatch", "exit"]) - } catch (e) { - Logger.error("Compositor", "Error logging out from Hyprland:", e) - } - } else if (isNiri) { - try { - Quickshell.execDetached(["niri", "msg", "action", "quit", "--skip-confirmation"]) - } catch (e) { - Logger.error("Compositor", "Error logging out from Niri:", e) - } + if (backend && backend.logout) { + backend.logout() } else { - Logger.warn("Compositor", "No supported compositor detected for logout") + Logger.warn("Compositor", "No backend available for logout") } } @@ -664,4 +166,4 @@ Singleton { function suspend() { Quickshell.execDetached(["systemctl", "suspend"]) } -} +} \ No newline at end of file diff --git a/Services/HyprlandService.qml b/Services/HyprlandService.qml new file mode 100644 index 00000000..99e1fdb4 --- /dev/null +++ b/Services/HyprlandService.qml @@ -0,0 +1,275 @@ +import QtQuick +import Quickshell +import Quickshell.Hyprland +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 + + // Hyprland-specific properties + property bool initialized: false + property var workspaceCache: ({}) + property var windowCache: ({}) + + // Debounce timer for updates + Timer { + id: updateTimer + interval: 50 + repeat: false + onTriggered: safeUpdate() + } + + // Initialization + function initialize() { + if (initialized) return + + try { + Hyprland.refreshWorkspaces() + Hyprland.refreshToplevels() + Qt.callLater(() => { + safeUpdateWorkspaces() + safeUpdateWindows() + }) + initialized = true + Logger.log("HyprlandService", "Initialized successfully") + } catch (e) { + Logger.error("HyprlandService", "Failed to initialize:", e) + } + } + + // Safe update wrapper + function safeUpdate() { + safeUpdateWindows() + safeUpdateWorkspaces() + windowListChanged() + } + + // Safe workspace update + function safeUpdateWorkspaces() { + try { + workspaces.clear() + workspaceCache = {} + + if (!Hyprland.workspaces || !Hyprland.workspaces.values) { + return + } + + const hlWorkspaces = Hyprland.workspaces.values + const occupiedIds = getOccupiedWorkspaceIds() + + 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": occupiedIds[ws.id] === true + } + + workspaceCache[ws.id] = wsData + workspaces.append(wsData) + } + } catch (e) { + Logger.error("HyprlandService", "Error updating workspaces:", e) + } + } + + // Get occupied workspace IDs safely + function getOccupiedWorkspaceIds() { + const occupiedIds = {} + + try { + if (!Hyprland.toplevels || !Hyprland.toplevels.values) { + return occupiedIds + } + + const hlToplevels = Hyprland.toplevels.values + for (var i = 0; i < hlToplevels.length; i++) { + const toplevel = hlToplevels[i] + if (!toplevel) continue + + try { + const wsId = toplevel.workspace ? toplevel.workspace.id : null + if (wsId !== null && wsId !== undefined) { + occupiedIds[wsId] = true + } + } catch (e) { + // Ignore individual toplevel errors + } + } + } catch (e) { + // Return empty if we can't determine occupancy + } + + return occupiedIds + } + + // Safe window update + function safeUpdateWindows() { + try { + const windowsList = [] + windowCache = {} + + if (!Hyprland.toplevels || !Hyprland.toplevels.values) { + windows = [] + focusedWindowIndex = -1 + return + } + + const hlToplevels = Hyprland.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) + windowCache[windowData.id] = windowData + + if (windowData.isFocused) { + newFocusedIndex = windowsList.length - 1 + } + } + } + + windows = windowsList + + if (newFocusedIndex !== focusedWindowIndex) { + focusedWindowIndex = newFocusedIndex + activeWindowChanged() + } + } catch (e) { + Logger.error("HyprlandService", "Error updating windows:", e) + } + } + + // Extract window data safely from a toplevel + function extractWindowData(toplevel) { + if (!toplevel) return null + + try { + // Safely extract properties + const windowId = safeGetProperty(toplevel, "address", "") + if (!windowId) return null + + const appId = extractAppId(toplevel) + const title = safeGetProperty(toplevel, "title", "") + const wsId = toplevel.workspace ? toplevel.workspace.id : null + const focused = toplevel.activated === true + + return { + "id": windowId, + "title": title, + "appId": appId, + "workspaceId": wsId, + "isFocused": focused + } + } catch (e) { + return null + } + } + + // Extract app ID from various possible sources + function extractAppId(toplevel) { + if (!toplevel) return "" + + // Try direct properties + var appId = safeGetProperty(toplevel, "class", "") + if (appId) return appId + + appId = safeGetProperty(toplevel, "initialClass", "") + if (appId) return appId + + appId = safeGetProperty(toplevel, "appId", "") + if (appId) return appId + + // Try lastIpcObject + try { + const ipcData = toplevel.lastIpcObject + if (ipcData) { + return String(ipcData.class || ipcData.initialClass || + ipcData.appId || ipcData.wm_class || "") + } + } catch (e) { + // Ignore IPC errors + } + + return "" + } + + // 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 Hyprland + Connections { + target: Hyprland.workspaces + enabled: initialized + function onValuesChanged() { + safeUpdateWorkspaces() + workspaceChanged() + } + } + + Connections { + target: Hyprland.toplevels + enabled: initialized + function onValuesChanged() { + updateTimer.restart() + } + } + + Connections { + target: Hyprland + enabled: initialized + function onRawEvent(event) { + safeUpdateWorkspaces() + workspaceChanged() + updateTimer.restart() + } + } + + // Public functions + function switchToWorkspace(workspaceId) { + try { + Hyprland.dispatch(`workspace ${workspaceId}`) + } catch (e) { + Logger.error("HyprlandService", "Failed to switch workspace:", e) + } + } + + function logout() { + try { + Quickshell.execDetached(["hyprctl", "dispatch", "exit"]) + } catch (e) { + Logger.error("HyprlandService", "Failed to logout:", e) + } + } +} \ No newline at end of file diff --git a/Services/NiriService.qml b/Services/NiriService.qml new file mode 100644 index 00000000..1929bc97 --- /dev/null +++ b/Services/NiriService.qml @@ -0,0 +1,295 @@ +import QtQuick +import Quickshell +import Quickshell.Io +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 + + // Initialization + function initialize() { + niriEventStream.running = true + updateWorkspaces() + updateWindows() + Logger.log("NiriService", "Initialized successfully") + } + + // Update workspaces + function updateWorkspaces() { + niriWorkspaceProcess.running = true + } + + // Update windows + function updateWindows() { + niriWindowsProcess.running = true + } + + // Niri workspace process + Process { + id: niriWorkspaceProcess + running: false + command: ["niri", "msg", "--json", "workspaces"] + + stdout: SplitParser { + onRead: function (line) { + try { + const workspacesData = JSON.parse(line) + const workspacesList = [] + + for (const ws of workspacesData) { + workspacesList.push({ + "id": ws.id, + "idx": ws.idx, + "name": ws.name || "", + "output": ws.output || "", + "isFocused": ws.is_focused === true, + "isActive": ws.is_active === true, + "isUrgent": ws.is_urgent === true, + "isOccupied": ws.active_window_id ? true : false + }) + } + + // Sort workspaces by output, then by index + workspacesList.sort((a, b) => { + if (a.output !== b.output) { + return a.output.localeCompare(b.output) + } + return a.idx - b.idx + }) + + // Update the workspaces ListModel + workspaces.clear() + for (var i = 0; i < workspacesList.length; i++) { + workspaces.append(workspacesList[i]) + } + + workspaceChanged() + } catch (e) { + Logger.error("NiriService", "Failed to parse workspaces:", e, line) + } + } + } + } + + // Niri windows process (for initial load) + Process { + id: niriWindowsProcess + running: false + command: ["niri", "msg", "--json", "windows"] + + stdout: SplitParser { + onRead: function (line) { + try { + const windowsData = JSON.parse(line) + const windowsList = [] + + for (const win of windowsData) { + windowsList.push({ + "id": win.id, + "title": win.title || "", + "appId": win.app_id || "", + "workspaceId": win.workspace_id || null, + "isFocused": win.is_focused === true + }) + } + + windowsList.sort((a, b) => a.id - b.id) + windows = windowsList + windowListChanged() + + // Update focused window index + focusedWindowIndex = -1 + for (var i = 0; i < windowsList.length; i++) { + if (windowsList[i].isFocused) { + focusedWindowIndex = i + break + } + } + + activeWindowChanged() + } catch (e) { + Logger.error("NiriService", "Failed to parse windows:", e, line) + } + } + } + } + + // Niri event stream process + Process { + id: niriEventStream + running: false + command: ["niri", "msg", "--json", "event-stream"] + + stdout: SplitParser { + onRead: data => { + try { + const event = JSON.parse(data.trim()) + + if (event.WorkspacesChanged) { + updateWorkspaces() + } + else if (event.WindowOpenedOrChanged) { + handleWindowOpenedOrChanged(event.WindowOpenedOrChanged) + } + else if (event.WindowClosed) { + handleWindowClosed(event.WindowClosed) + } + else if (event.WindowsChanged) { + handleWindowsChanged(event.WindowsChanged) + } + else if (event.WorkspaceActivated) { + updateWorkspaces() + } + else if (event.WindowFocusChanged) { + handleWindowFocusChanged(event.WindowFocusChanged) + } + // Removed OverviewOpenedOrClosed handling + } catch (e) { + Logger.error("NiriService", "Error parsing event stream:", e, data) + } + } + } + } + + // Event handlers + function handleWindowOpenedOrChanged(eventData) { + try { + const windowData = eventData.window + const existingIndex = windows.findIndex(w => w.id === windowData.id) + + const newWindow = { + "id": windowData.id, + "title": windowData.title || "", + "appId": windowData.app_id || "", + "workspaceId": windowData.workspace_id || null, + "isFocused": windowData.is_focused === true + } + + if (existingIndex >= 0) { + // Update existing window + windows[existingIndex] = newWindow + } else { + // Add new window + windows.push(newWindow) + windows.sort((a, b) => a.id - b.id) + } + + // Update focused window index if this window is focused + if (newWindow.isFocused) { + const oldFocusedIndex = focusedWindowIndex + focusedWindowIndex = windows.findIndex(w => w.id === windowData.id) + + // Only emit activeWindowChanged if the focused window actually changed + if (oldFocusedIndex !== focusedWindowIndex) { + activeWindowChanged() + } + } + + windowListChanged() + } catch (e) { + Logger.error("NiriService", "Error handling WindowOpenedOrChanged:", e) + } + } + + function handleWindowClosed(eventData) { + try { + const windowId = eventData.id + const windowIndex = windows.findIndex(w => w.id === windowId) + + if (windowIndex >= 0) { + // If this was the focused window, clear focus + if (windowIndex === focusedWindowIndex) { + focusedWindowIndex = -1 + activeWindowChanged() + } else if (focusedWindowIndex > windowIndex) { + // Adjust focused window index if needed + focusedWindowIndex-- + } + + // Remove the window + windows.splice(windowIndex, 1) + windowListChanged() + } + } catch (e) { + Logger.error("NiriService", "Error handling WindowClosed:", e) + } + } + + function handleWindowsChanged(eventData) { + try { + const windowsData = eventData.windows + const windowsList = [] + + for (const win of windowsData) { + windowsList.push({ + "id": win.id, + "title": win.title || "", + "appId": win.app_id || "", + "workspaceId": win.workspace_id || null, + "isFocused": win.is_focused === true + }) + } + + windowsList.sort((a, b) => a.id - b.id) + windows = windowsList + windowListChanged() + + // Update focused window index + focusedWindowIndex = -1 + for (var i = 0; i < windowsList.length; i++) { + if (windowsList[i].isFocused) { + focusedWindowIndex = i + break + } + } + + activeWindowChanged() + } catch (e) { + Logger.error("NiriService", "Error handling WindowsChanged:", e) + } + } + + function handleWindowFocusChanged(eventData) { + try { + const focusedId = eventData.id + + if (focusedId) { + const newIndex = windows.findIndex(w => w.id === focusedId) + focusedWindowIndex = newIndex >= 0 ? newIndex : -1 + } else { + focusedWindowIndex = -1 + } + + activeWindowChanged() + } catch (e) { + Logger.error("NiriService", "Error handling WindowFocusChanged:", e) + } + } + + // Public functions + function switchToWorkspace(workspaceId) { + try { + Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]) + } catch (e) { + Logger.error("NiriService", "Failed to switch workspace:", e) + } + } + + function logout() { + try { + Quickshell.execDetached(["niri", "msg", "action", "quit", "--skip-confirmation"]) + } catch (e) { + Logger.error("NiriService", "Failed to logout:", e) + } + } +} \ No newline at end of file diff --git a/Services/WorkspaceService.qml b/Services/WorkspaceService.qml deleted file mode 100644 index e4c24bea..00000000 --- a/Services/WorkspaceService.qml +++ /dev/null @@ -1,47 +0,0 @@ -pragma Singleton - -import QtQuick -import Quickshell -import qs.Commons -import qs.Services - -Singleton { - id: root - - // Delegate to CompositorService for all workspace operations - property ListModel workspaces: ListModel {} - property bool isHyprland: false - property bool isNiri: false - - Component.onCompleted: { - // Connect to CompositorService workspace changes - CompositorService.workspaceChanged.connect(updateWorkspaces) - // Initial sync - updateWorkspaces() - } - - // Listen to compositor detection changes - Connections { - target: CompositorService - function onIsHyprlandChanged() { - isHyprland = CompositorService.isHyprland - } - function onIsNiriChanged() { - isNiri = CompositorService.isNiri - } - } - - function updateWorkspaces() { - workspaces.clear() - for (var i = 0; i < CompositorService.workspaces.count; i++) { - const ws = CompositorService.workspaces.get(i) - workspaces.append(ws) - } - // Explicitly trigger the signal to ensure the Workspace module gets notified - workspacesChanged() - } - - function switchToWorkspace(workspaceId) { - CompositorService.switchToWorkspace(workspaceId) - } -}