pragma Singleton import QtQuick import Quickshell import Quickshell.Io import qs.Commons import qs.Services.UI Singleton { id: root // Compositor detection property bool isHyprland: false property bool isNiri: false property bool isSway: false property bool isMango: 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 Component.onCompleted: { // Load display scales from ShellState Qt.callLater(() => { if (typeof ShellState !== 'undefined' && ShellState.isLoaded) { loadDisplayScalesFromState(); } }); detectCompositor(); } Connections { target: typeof ShellState !== 'undefined' ? ShellState : null function onIsLoadedChanged() { if (ShellState.isLoaded) { loadDisplayScalesFromState(); } } } function detectCompositor() { const hyprlandSignature = Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE"); const niriSocket = Quickshell.env("NIRI_SOCKET"); const swaySock = Quickshell.env("SWAYSOCK"); const currentDesktop = Quickshell.env("XDG_CURRENT_DESKTOP"); // Check for MangoWC using XDG_CURRENT_DESKTOP environment variable // MangoWC sets XDG_CURRENT_DESKTOP=mango if (currentDesktop && currentDesktop.toLowerCase().includes("mango")) { isHyprland = false; isNiri = false; isSway = false; isMango = true; backendLoader.sourceComponent = mangoComponent; } else if (niriSocket && niriSocket.length > 0) { isHyprland = false; isNiri = true; isSway = false; isMango = false; backendLoader.sourceComponent = niriComponent; } else if (hyprlandSignature && hyprlandSignature.length > 0) { isHyprland = true; isNiri = false; isSway = false; isMango = false; backendLoader.sourceComponent = hyprlandComponent; } else if (swaySock && swaySock.length > 0) { isHyprland = false; isNiri = false; isSway = true; isMango = false; backendLoader.sourceComponent = swayComponent; } else { // Always fallback to Niri isHyprland = false; isNiri = true; isSway = false; isMango = false; backendLoader.sourceComponent = niriComponent; } } Loader { id: backendLoader onLoaded: { if (item) { root.backend = item; setupBackendConnections(); backend.initialize(); } } } // Load display scales from ShellState function loadDisplayScalesFromState() { try { const cached = ShellState.getDisplay(); if (cached && Object.keys(cached).length > 0) { displayScales = cached; displayScalesLoaded = true; Logger.d("CompositorService", "Loaded display scales from ShellState"); } else { // Try to migrate from old display.json if it exists migrateFromOldDisplayFile(); } } catch (error) { Logger.e("CompositorService", "Failed to load display scales:", error); displayScalesLoaded = true; } } // Migration from old display.json file function migrateFromOldDisplayFile() { const oldDisplayPath = Settings.cacheDir + "display.json"; const migrationFileView = Qt.createQmlObject(` import QtQuick import Quickshell.Io FileView { id: migrationView path: "${oldDisplayPath}" printErrors: false adapter: JsonAdapter { property var displays: ({}) } onLoaded: { parent.displayScales = adapter.displays || {}; parent.displayScalesLoaded = true; parent.saveDisplayScalesToCache(); Logger.i("CompositorService", "Migrated display.json to ShellState"); migrationView.destroy(); } onLoadFailed: { parent.displayScalesLoaded = true; migrationView.destroy(); } } `, root, "migrationFileView"); } // 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 } } // Mango backend component Component { id: mangoComponent MangoService { id: mangoBackend } } 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 - use automatic property change signal 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.d("CompositorService", "Display scales updated"); } // Save display scales to cache function saveDisplayScalesToCache() { try { ShellState.setDisplay(displayScales); Logger.d("CompositorService", "Saved display scales to ShellState"); } catch (error) { Logger.e("CompositorService", "Failed to save display scales:", error); } } // 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 ""; } // Get clean app name from appId // Extracts the last segment from reverse domain notation (e.g., "org.kde.dolphin" -> "Dolphin") // Falls back to title if appId is empty function getCleanAppName(appId, fallbackTitle) { var name = (appId || "").split(".").pop() || fallbackTitle || "Unknown"; return name.charAt(0).toUpperCase() + name.slice(1); } function getWindowsForWorkspace(workspaceId) { var windowsInWs = []; for (var i = 0; i < windows.count; i++) { var window = windows.get(i); if (window.workspaceId === workspaceId) { windowsInWs.push(window); } } return windowsInWs; } // 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(["sh", "-c", "systemctl poweroff || loginctl poweroff"]); } function reboot() { Logger.i("Compositor", "Reboot requested"); Quickshell.execDetached(["sh", "-c", "systemctl reboot || loginctl reboot"]); } function suspend() { Logger.i("Compositor", "Suspend requested"); Quickshell.execDetached(["sh", "-c", "systemctl suspend || loginctl suspend"]); } function hibernate() { Logger.i("Compositor", "Hibernate requested"); Quickshell.execDetached(["sh", "-c", "systemctl hibernate || loginctl hibernate"]); } 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); } }