import QtQuick import Quickshell import Quickshell.I3 import Quickshell.Io import Quickshell.Wayland import qs.Commons import qs.Services.Keyboard Item { id: root // Configurable IPC command name (overridden to "scrollmsg" for Scroll) property string msgCommand: "swaymsg" // 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 signal displayScalesChanged // I3-specific properties property bool initialized: false // Cache for window-to-workspace mapping property var windowWorkspaceMap: ({}) // Track window usage counts per workspace to handle duplicates property var windowUsageCountsPerWorkspace: ({}) // 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(); queryWindowWorkspaces(); queryDisplayScales(); queryKeyboardLayout(); }); initialized = true; Logger.i("SwayService", "Service started"); } catch (e) { Logger.e("SwayService", "Failed to initialize:", e); } } // Query window-to-workspace mapping via IPC function queryWindowWorkspaces() { swayTreeProcess.running = true; } // Sway tree process for getting window workspace information Process { id: swayTreeProcess running: false command: [msgCommand, "-t", "get_tree", "-r"] property string accumulatedOutput: "" stdout: SplitParser { onRead: function (line) { swayTreeProcess.accumulatedOutput += line; } } onExited: function (exitCode) { if (exitCode !== 0 || !accumulatedOutput) { Logger.e("SwayService", "Failed to query tree, exit code:", exitCode); accumulatedOutput = ""; return; } try { const treeData = JSON.parse(accumulatedOutput); const newMap = {}; const workspaceWindows = {}; // Track windows per workspace // Recursively find all windows and their workspaces function traverseTree(node, workspaceNum) { if (!node) return; // If this is a workspace node, update the workspace number if (node.type === "workspace" && node.num !== undefined) { workspaceNum = node.num; if (!workspaceWindows[workspaceNum]) { workspaceWindows[workspaceNum] = []; } } // If this is a regular or floating container with app_id/class (i.e., a window) if ((node.type === "con" || node.type === "floating_con") && (node.app_id || node.window_properties)) { const appId = node.app_id || (node.window_properties ? node.window_properties.class : null); const title = node.name || ""; const id = node.id; if (appId && workspaceNum !== undefined && workspaceNum >= 0) { // Store window info for this workspace workspaceWindows[workspaceNum].push({ appId: appId, title: title, id: id }); } } // Traverse children if (node.nodes && node.nodes.length > 0) { for (const child of node.nodes) { traverseTree(child, workspaceNum); } } // Traverse floating nodes if (node.floating_nodes && node.floating_nodes.length > 0) { for (const child of node.floating_nodes) { traverseTree(child, workspaceNum); } } } traverseTree(treeData, -1); // Now build the map with workspace-specific keys for (const wsNum in workspaceWindows) { const windows = workspaceWindows[wsNum]; const appTitleCounts = {}; // Count occurrences of each appId:title in this workspace for (const win of windows) { const baseKey = `${win.appId}:${win.title}`; // Track how many times we've seen this appId:title combo in this workspace if (!appTitleCounts[baseKey]) { appTitleCounts[baseKey] = 0; } const occurrence = appTitleCounts[baseKey]; appTitleCounts[baseKey]++; // Create unique key with workspace and occurrence index const uniqueKey = `ws${wsNum}:${baseKey}[${occurrence}]`; newMap[uniqueKey] = parseInt(wsNum); // Also store by ID if available (most reliable) if (win.id) { newMap[`id:${win.id}`] = parseInt(wsNum); } } } windowWorkspaceMap = newMap; // Update windows with new workspace information Qt.callLater(safeUpdateWindows); } catch (e) { Logger.e("SwayService", "Failed to parse tree:", e); } finally { accumulatedOutput = ""; } } } // Query display scales function queryDisplayScales() { swayOutputsProcess.running = true; } // Sway outputs process for display scale detection Process { id: swayOutputsProcess running: false command: [msgCommand, "-t", "get_outputs", "-r"] property string accumulatedOutput: "" stdout: SplitParser { onRead: function (line) { swayOutputsProcess.accumulatedOutput += line; } } onExited: function (exitCode) { if (exitCode !== 0 || !accumulatedOutput) { Logger.e("SwayService", "Failed to query outputs, exit code:", exitCode); accumulatedOutput = ""; return; } try { const outputsData = JSON.parse(accumulatedOutput); const scales = {}; for (const output of outputsData) { if (output.name) { scales[output.name] = { "name": output.name, "scale": output.scale || 1.0, "width": output.current_mode ? output.current_mode.width : 0, "height": output.current_mode ? output.current_mode.height : 0, "refresh_rate": output.current_mode ? output.current_mode.refresh : 0, "x": output.rect ? output.rect.x : 0, "y": output.rect ? output.rect.y : 0, "active": output.active || false, "focused": output.focused || false, "current_workspace": output.current_workspace || "" }; } } // Notify CompositorService (it will emit displayScalesChanged) if (CompositorService && CompositorService.onDisplayScalesUpdated) { CompositorService.onDisplayScalesUpdated(scales); } } catch (e) { Logger.e("SwayService", "Failed to parse outputs:", e); } finally { // Clear accumulated output for next query accumulatedOutput = ""; } } } function queryKeyboardLayout() { swayInputsProcess.running = true; } // Sway inputs process for keyboard layout detection Process { id: swayInputsProcess running: false command: [msgCommand, "-t", "get_inputs", "-r"] property string accumulatedOutput: "" stdout: SplitParser { onRead: function (line) { // Accumulate lines instead of parsing each one swayInputsProcess.accumulatedOutput += line; } } onExited: function (exitCode) { if (exitCode !== 0 || !accumulatedOutput) { Logger.e("SwayService", "Failed to query inputs, exit code:", exitCode); accumulatedOutput = ""; return; } try { const inputsData = JSON.parse(accumulatedOutput); for (const input of inputsData) { if (input.type == "keyboard") { const layoutName = input.xkb_active_layout_name; KeyboardLayoutService.setCurrentLayout(layoutName); Logger.d("SwayService", "Keyboard layout switched:", layoutName); break; } } } catch (e) { Logger.e("SwayService", "Failed to parse inputs:", e); } finally { // Clear accumulated output for next query accumulatedOutput = ""; } } } // Safe update wrapper function safeUpdate() { queryWindowWorkspaces(); safeUpdateWorkspaces(); } // 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.num, "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.e("SwayService", "Error updating workspaces:", e); } } // Safe window update function safeUpdateWindows() { try { const windowsList = []; // Reset usage counts per workspace before processing windows windowUsageCountsPerWorkspace = {}; if (!ToplevelManager.toplevels || !ToplevelManager.toplevels.values) { windows = []; focusedWindowIndex = -1; windowListChanged(); 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(); } windowListChanged(); } catch (e) { Logger.e("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 = getAppId(toplevel); const title = safeGetProperty(toplevel, "title", ""); const focused = toplevel.activated === true; // Try to find workspace ID from our cached map by trying all workspaces let workspaceId = -1; let foundWorkspaceNum = -1; // Build base key for this window const baseKey = `${appId}:${title}`; // Try to find this window in any workspace for (var i = 0; i < workspaces.count; i++) { const ws = workspaces.get(i); if (!ws) continue; const wsNum = ws.idx; // Initialize usage count for this workspace if needed if (!windowUsageCountsPerWorkspace[wsNum]) { windowUsageCountsPerWorkspace[wsNum] = {}; } // Get current usage count for this appId:title in this workspace if (!windowUsageCountsPerWorkspace[wsNum][baseKey]) { windowUsageCountsPerWorkspace[wsNum][baseKey] = 0; } const occurrence = windowUsageCountsPerWorkspace[wsNum][baseKey]; const uniqueKey = `ws${wsNum}:${baseKey}[${occurrence}]`; // Check if this key exists in our map if (windowWorkspaceMap[uniqueKey] !== undefined) { foundWorkspaceNum = windowWorkspaceMap[uniqueKey]; workspaceId = ws.id; // Increment the usage count for this workspace windowUsageCountsPerWorkspace[wsNum][baseKey]++; break; } } return { "title": title, "appId": appId, "isFocused": focused, "workspaceId": workspaceId, "handle": toplevel }; } catch (e) { return null; } } function getAppId(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; } function handleInputEvent(ev) { try { const eventData = JSON.parse(ev); if (eventData.change == "xkb_layout" && eventData.input != null) { const input = eventData.input; if (input.type == "keyboard" && input.xkb_active_layout_name != null) { const layoutName = input.xkb_active_layout_name; KeyboardLayoutService.setCurrentLayout(layoutName); Logger.d("SwayService", "Keyboard layout switched:", layoutName); } } } catch (e) { Logger.e("SwayService", "Error handling input event:", e); } } // Connections to I3 Connections { target: I3.workspaces enabled: initialized function onValuesChanged() { safeUpdateWorkspaces(); workspaceChanged(); } } Connections { target: ToplevelManager enabled: initialized function onActiveToplevelChanged() { updateTimer.restart(); } } // Some programs change title of window dependent on content Connections { target: ToplevelManager ? ToplevelManager.activeToplevel : null enabled: initialized function onTitleChanged() { updateTimer.restart(); } } Connections { target: I3 enabled: initialized function onRawEvent(event) { safeUpdateWorkspaces(); workspaceChanged(); updateTimer.restart(); if (event.type === "output") { Qt.callLater(queryDisplayScales); } } } I3IpcListener { subscriptions: ["input"] onIpcEvent: function (event) { handleInputEvent(event.data); } } // Public functions function switchToWorkspace(workspace) { try { workspace.handle.activate(); } catch (e) { Logger.e("SwayService", "Failed to switch workspace:", e); } } function focusWindow(window) { try { window.handle.activate(); } catch (e) { Logger.e("SwayService", "Failed to switch window:", e); } } function closeWindow(window) { try { window.handle.close(); } catch (e) { Logger.e("SwayService", "Failed to close window:", e); } } function turnOffMonitors() { try { Quickshell.execDetached([msgCommand, "output", "*", "dpms", "off"]); } catch (e) { Logger.e("SwayService", "Failed to turn off monitors:", e); } } function turnOnMonitors() { try { Quickshell.execDetached([msgCommand, "output", "*", "dpms", "on"]); } catch (e) { Logger.e("SwayService", "Failed to turn on monitors:", e); } } function logout() { try { Quickshell.execDetached([msgCommand, "exit"]); } catch (e) { Logger.e("SwayService", "Failed to logout:", e); } } function cycleKeyboardLayout() { try { Quickshell.execDetached([msgCommand, "input", "type:keyboard", "xkb_switch_layout", "next"]); } catch (e) { Logger.e("SwayService", "Failed to cycle keyboard layout:", e); } } function getFocusedScreen() { // de-activated until proper testing return null; // const i3Mon = I3.focusedMonitor; // if (i3Mon) { // const monitorName = i3Mon.name; // for (let i = 0; i < Quickshell.screens.length; i++) { // if (Quickshell.screens[i].name === monitorName) { // return Quickshell.screens[i]; // } // } // } // return null; } function spawn(command) { try { Quickshell.execDetached([msgCommand, "exec", "--"].concat(command)); } catch (e) { Logger.e("SwayService", "Failed to spawn command:", e); } } }