pragma Singleton import QtQuick import Quickshell import Quickshell.Io import qs.Commons import qs.Services.Control 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 property bool isLabwc: false property bool isScroll: 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 // Overview state (Niri-specific, defaults to false for other compositors) property bool overviewActive: false // Global workspaces flag (workspaces shared across all outputs) // True for LabWC (stacking compositor), false for tiling WMs with per-output workspaces property bool globalWorkspaces: 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"); const labwcPid = Quickshell.env("LABWC_PID"); // 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; isLabwc = false; backendLoader.sourceComponent = mangoComponent; } else if (labwcPid && labwcPid.length > 0) { isHyprland = false; isNiri = false; isSway = false; isMango = false; isLabwc = true; backendLoader.sourceComponent = labwcComponent; Logger.i("CompositorService", "Detected LabWC with PID: " + labwcPid); } else if (niriSocket && niriSocket.length > 0) { isHyprland = false; isNiri = true; isSway = false; isMango = false; isLabwc = false; backendLoader.sourceComponent = niriComponent; } else if (hyprlandSignature && hyprlandSignature.length > 0) { isHyprland = true; isNiri = false; isSway = false; isMango = false; isLabwc = false; backendLoader.sourceComponent = hyprlandComponent; } else if (swaySock && swaySock.length > 0) { isHyprland = false; isNiri = false; isSway = true; isMango = false; isLabwc = false; isScroll = currentDesktop && currentDesktop.toLowerCase().includes("scroll"); backendLoader.sourceComponent = swayComponent; } else { // Always fallback to Niri isHyprland = false; isNiri = true; isSway = false; isMango = false; isLabwc = false; backendLoader.sourceComponent = niriComponent; } } Loader { id: backendLoader onLoaded: { if (item) { if (isScroll) { item.msgCommand = "scrollmsg"; } 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 { // Migration is now handled in Settings.qml displayScalesLoaded = true; } } catch (error) { Logger.e("CompositorService", "Failed to load display scales:", error); displayScalesLoaded = true; } } // 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 } } // Labwc backend component Component { id: labwcComponent LabwcService { id: labwcBackend } } 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(() => { // Only sync focus state, not entire window list syncFocusedWindow(); // 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; }); // Overview state (Niri-specific) if (backend.overviewActiveChanged) { backend.overviewActiveChanged.connect(() => { overviewActive = backend.overviewActive; }); } // Initial sync syncWorkspaces(); syncWindows(); focusedWindowIndex = backend.focusedWindowIndex; if (backend.overviewActive !== undefined) { overviewActive = backend.overviewActive; } if (backend.globalWorkspaces !== undefined) { globalWorkspaces = backend.globalWorkspaces; } } 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 window list has been updated windowListChanged(); } // Sync only the focused window state, not the entire window list function syncFocusedWindow() { const newIndex = backend.focusedWindowIndex; // Update isFocused flags by syncing from backend for (var i = 0; i < windows.count && i < backend.windows.length; i++) { const backendFocused = backend.windows[i].isFocused; if (windows.get(i).isFocused !== backendFocused) { windows.setProperty(i, "isFocused", backendFocused); } } focusedWindowIndex = newIndex; } // 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(); 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 screen from compositor function getFocusedScreen() { if (backend && backend.getFocusedScreen) { return backend.getFocusedScreen(); } 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"); } } // Spawn command function spawn(command) { // Ensure command is a proper JS array (QML lists can behave unexpectedly in some contexts) const cmdArray = Array.isArray(command) ? command : (command && typeof command === "object" && command.length !== undefined) ? Array.from(command) : [command]; Logger.d("CompositorService", `Spawning: ${cmdArray.join(" ")}`); if (backend && backend.spawn) { backend.spawn(cmdArray); } else { try { Quickshell.execDetached(cmdArray); } catch (e) { Logger.e("CompositorService", "Failed to execute detached:", e); } } } // Session management helper for custom commands function getCustomCommand(action) { const powerOptions = Settings.data.sessionMenu.powerOptions || []; for (let i = 0; i < powerOptions.length; i++) { const option = powerOptions[i]; if (option.action === action && option.enabled && option.command && option.command.trim() !== "") { return option.command.trim(); } } return ""; } function executeSessionAction(action, defaultCommand) { const customCommand = getCustomCommand(action); if (customCommand) { Logger.i("Compositor", `Executing custom command for action: ${action} Command: ${customCommand}`); Quickshell.execDetached(["sh", "-c", customCommand]); return true; } return false; } // Session management function logout() { Logger.i("Compositor", "Logout requested"); if (executeSessionAction("logout")) return; if (backend && backend.logout) { backend.logout(); } else { Logger.w("Compositor", "No backend available for logout"); } } function shutdown() { Logger.i("Compositor", "Shutdown requested"); if (executeSessionAction("shutdown")) return; HooksService.executeSessionHook("shutdown", () => { Quickshell.execDetached(["sh", "-c", "systemctl poweroff || loginctl poweroff"]); }); } function reboot() { Logger.i("Compositor", "Reboot requested"); if (executeSessionAction("reboot")) return; HooksService.executeSessionHook("reboot", () => { Quickshell.execDetached(["sh", "-c", "systemctl reboot || loginctl reboot"]); }); } function rebootToUefi() { Logger.i("Compositor", "Reboot to UEFI firmware requested requested"); if (executeSessionAction("rebootToUefi")) return; HooksService.executeSessionHook("rebootToUefi", () => { Quickshell.execDetached(["sh", "-c", "systemctl reboot --firmware-setup"]); }); } function suspend() { Logger.i("Compositor", "Suspend requested"); if (executeSessionAction("suspend")) return; Quickshell.execDetached(["sh", "-c", "systemctl suspend || loginctl suspend"]); } function lock() { Logger.i("Compositor", "LockScreen requested"); if (executeSessionAction("lock")) return; if (PanelService && PanelService.lockScreen) { PanelService.lockScreen.active = true; } } function hibernate() { Logger.i("Compositor", "Hibernate requested"); if (executeSessionAction("hibernate")) return; Quickshell.execDetached(["sh", "-c", "systemctl hibernate || loginctl hibernate"]); } function cycleKeyboardLayout() { if (backend && backend.cycleKeyboardLayout) { backend.cycleKeyboardLayout(); } } property int lockAndSuspendCheckCount: 0 function lockAndSuspend() { Logger.i("Compositor", "Lock and suspend requested"); // if a custom lock command exists, execute it and suspend without wait if (executeSessionAction("lock")) { suspend(); return; } // If already locked, suspend immediately if (PanelService && PanelService.lockScreen && PanelService.lockScreen.active) { Logger.i("Compositor", "Screen already locked, suspending"); suspend(); return; } // Lock the screen first try { if (PanelService && PanelService.lockScreen) { PanelService.lockScreen.active = true; lockAndSuspendCheckCount = 0; // Wait for lock screen to be confirmed active before suspending lockAndSuspendTimer.start(); } else { Logger.w("Compositor", "Lock screen not available, suspending without lock"); suspend(); } } catch (e) { Logger.w("Compositor", "Failed to activate lock screen before suspend: " + e); suspend(); } } Timer { id: lockAndSuspendTimer interval: 100 repeat: true running: false onTriggered: { lockAndSuspendCheckCount++; // Check if lock screen is now active if (PanelService && PanelService.lockScreen && PanelService.lockScreen.active) { // Verify the lock screen component is loaded if (PanelService.lockScreen.item) { Logger.i("Compositor", "Lock screen confirmed active, suspending"); stop(); lockAndSuspendCheckCount = 0; suspend(); } else { // Lock screen is active but component not loaded yet, wait a bit more if (lockAndSuspendCheckCount > 20) { // Max 2 seconds wait Logger.w("Compositor", "Lock screen active but component not loaded, suspending anyway"); stop(); lockAndSuspendCheckCount = 0; suspend(); } } } else { // Lock screen not active yet, keep checking if (lockAndSuspendCheckCount > 30) { // Max 3 seconds wait Logger.w("Compositor", "Lock screen failed to activate, suspending anyway"); stop(); lockAndSuspendCheckCount = 0; suspend(); } } } } }