pragma Singleton import QtQuick import Quickshell import Quickshell.Io import qs.Commons import qs.Services Singleton { id: root // Compositor detection property bool isHyprland: false property bool isNiri: false property bool isSway: false // Generic workspace and window data property ListModel workspaces: ListModel {} property ListModel windows: ListModel {} property int focusedWindowIndex: -1 // Display scale data property var displayScales: ({}) property bool displayScalesLoaded: false // Generic events signal workspaceChanged signal activeWindowChanged signal windowListChanged // Backend service loader property var backend: null // Cache file path property string displayCachePath: "" Component.onCompleted: { // Setup cache path (needs Settings to be available) Qt.callLater(() => { if (typeof Settings !== 'undefined' && Settings.cacheDir) { displayCachePath = Settings.cacheDir + "display.json" displayCacheFileView.path = displayCachePath } }) detectCompositor() } function detectCompositor() { const hyprlandSignature = Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE") const niriSocket = Quickshell.env("NIRI_SOCKET") const swaySock = Quickshell.env("SWAYSOCK") if (niriSocket && niriSocket.length > 0) { isHyprland = false isNiri = true isSway = false backendLoader.sourceComponent = niriComponent } else 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 { // Always fallback to Niri isHyprland = false isNiri = true isSway = false backendLoader.sourceComponent = niriComponent } } Loader { id: backendLoader onLoaded: { if (item) { root.backend = item setupBackendConnections() backend.initialize() } } } // Cache FileView for display scales FileView { id: displayCacheFileView printErrors: false watchChanges: false adapter: JsonAdapter { id: displayCacheAdapter property var displays: ({}) } onLoaded: { // Load cached display scales displayScales = displayCacheAdapter.displays || {} displayScalesLoaded = true // Logger.i("CompositorService", "Loaded display scales from cache:", JSON.stringify(displayScales)) } onLoadFailed: { // Cache doesn't exist yet, will be created on first update displayScalesLoaded = true // Logger.i("CompositorService", "No display cache found, will create on first update") } } // Hyprland backend component Component { id: hyprlandComponent HyprlandService { id: hyprlandBackend } } // Niri backend component Component { id: niriComponent NiriService { id: niriBackend } } // Sway backend component Component { id: swayComponent SwayService { id: swayBackend } } 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(() => { // Sync active window when it changes // TODO: Avoid re-syncing all windows syncWindows() // Forward the signal activeWindowChanged() }) backend.windowListChanged.connect(() => { // Sync windows when they change syncWindows() // Forward the signal windowListChanged() }) // Property bindings backend.focusedWindowIndexChanged.connect(() => { focusedWindowIndex = backend.focusedWindowIndex }) // Initial sync syncWorkspaces() syncWindows() 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() } function syncWindows() { windows.clear() const ws = backend.windows for (var i = 0; i < ws.length; i++) { windows.append(ws[i]) } // Emit signal to notify listeners that workspace list has been updated windowListChanged() } // Update display scales from backend function updateDisplayScales() { if (!backend || !backend.queryDisplayScales) { Logger.w("CompositorService", "Backend does not support display scale queries") return } backend.queryDisplayScales() } // Called by backend when display scales are ready function onDisplayScalesUpdated(scales) { displayScales = scales saveDisplayScalesToCache() displayScalesChanged() Logger.i("CompositorService", "Display scales updated") } // Save display scales to cache function saveDisplayScalesToCache() { if (!displayCachePath) { return } displayCacheAdapter.displays = displayScales displayCacheFileView.writeAdapter() } // Public function to get scale for a specific display function getDisplayScale(displayName) { if (!displayName || !displayScales[displayName]) { return 1.0 } return displayScales[displayName].scale || 1.0 } // Public function to get all display info for a specific display function getDisplayInfo(displayName) { if (!displayName || !displayScales[displayName]) { return null } return displayScales[displayName] } // Get focused window function getFocusedWindow() { if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.count) { return windows.get(focusedWindowIndex) } return null } // Get focused window title function getFocusedWindowTitle() { if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.count) { var title = windows.get(focusedWindowIndex).title if (title !== undefined) { title = title.replace(/(\r\n|\n|\r)/g, "") } return title || "" } return "" } // Generic workspace switching function switchToWorkspace(workspace) { if (backend && backend.switchToWorkspace) { backend.switchToWorkspace(workspace) } else { Logger.w("Compositor", "No backend available for workspace switching") } } // Get current workspace function getCurrentWorkspace() { for (var i = 0; i < workspaces.count; i++) { const ws = workspaces.get(i) if (ws.isFocused) { return ws } } return null } // Get active workspaces function getActiveWorkspaces() { const activeWorkspaces = [] for (var i = 0; i < workspaces.count; i++) { const ws = workspaces.get(i) if (ws.isActive) { activeWorkspaces.push(ws) } } return activeWorkspaces } // Set focused window function focusWindow(window) { if (backend && backend.focusWindow) { backend.focusWindow(window) } else { Logger.w("Compositor", "No backend available for window focus") } } // Close window function closeWindow(window) { if (backend && backend.closeWindow) { backend.closeWindow(window) } else { Logger.w("Compositor", "No backend available for window closing") } } // Session management function logout() { if (backend && backend.logout) { Logger.i("Compositor", "Logout requested") backend.logout() } else { Logger.w("Compositor", "No backend available for logout") } } function shutdown() { Logger.i("Compositor", "Shutdown requested") Quickshell.execDetached(["shutdown", "-h", "now"]) } function reboot() { Logger.i("Compositor", "Reboot requested") Quickshell.execDetached(["reboot"]) } function suspend() { Logger.i("Compositor", "Suspend requested") Quickshell.execDetached(["systemctl", "suspend"]) } function lockAndSuspend() { Logger.i("Compositor", "Lock and suspend requested") try { if (PanelService && PanelService.lockScreen && !PanelService.lockScreen.active) { PanelService.lockScreen.active = true } } catch (e) { Logger.w("Compositor", "Failed to activate lock screen before suspend: " + e) } // Queue suspend to the next event loop cycle to allow lock UI to render Qt.callLater(suspend) } }