From e814ba58274ce27a3342b58df40cdeb4a65f5ce6 Mon Sep 17 00:00:00 2001 From: atheeq-rhxn Date: Thu, 13 Nov 2025 14:48:20 +0530 Subject: [PATCH 1/8] feat: Add MangoWC compositor support --- Services/Compositor/CompositorService.qml | 30 +- Services/Compositor/MangoService.qml | 627 ++++++++++++++++++++++ 2 files changed, 655 insertions(+), 2 deletions(-) create mode 100644 Services/Compositor/MangoService.qml diff --git a/Services/Compositor/CompositorService.qml b/Services/Compositor/CompositorService.qml index 0ce10ffa..11dc118d 100644 --- a/Services/Compositor/CompositorService.qml +++ b/Services/Compositor/CompositorService.qml @@ -13,6 +13,7 @@ Singleton { 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 {} @@ -50,30 +51,47 @@ Singleton { const hyprlandSignature = Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE") const niriSocket = Quickshell.env("NIRI_SOCKET") const swaySock = Quickshell.env("SWAYSOCK") - if (niriSocket && niriSocket.length > 0) { + 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 + Logger.i("CompositorService", "MangoWC detected via XDG_CURRENT_DESKTOP:", currentDesktop) + } 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: { @@ -134,6 +152,14 @@ Singleton { } } + // Mango backend component + Component { + id: mangoComponent + MangoService { + id: mangoBackend + } + } + function setupBackendConnections() { if (!backend) return @@ -161,7 +187,7 @@ Singleton { windowListChanged() }) - // Property bindings + // Property bindings - use automatic property change signal backend.focusedWindowIndexChanged.connect(() => { focusedWindowIndex = backend.focusedWindowIndex }) diff --git a/Services/Compositor/MangoService.qml b/Services/Compositor/MangoService.qml new file mode 100644 index 00000000..3c7967b4 --- /dev/null +++ b/Services/Compositor/MangoService.qml @@ -0,0 +1,627 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.Keyboard + +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 + signal displayScalesChanged + + // Mango-specific properties + property bool initialized: false + property bool overviewActive: false + property var tagCache: ({}) + property var windowCache: ({}) + property var monitorCache: ({}) + property string currentLayout: "" + property string currentKeyboardLayout: "" + + // Debounce timer for updates + Timer { + id: updateTimer + interval: 50 + repeat: false + onTriggered: safeUpdate() + } + + // Initialization + function initialize() { + if (initialized) + return + + try { + // Initial data fetch + updateWorkspaces() + updateWindows() + queryDisplayScales() + queryKeyboardLayout() + + // Start event watching + mangoEventStream.running = true + + initialized = true + Logger.i("MangoService", "Service started") + } catch (e) { + Logger.e("MangoService", "Failed to initialize:", e) + } + } + + // Update workspaces (tags in MangoWC) + function updateWorkspaces() { + mangoTagsProcess.running = true + } + + // Update windows + function updateWindows() { + mangoWindowsProcess.running = true + } + + // Query display scales + function queryDisplayScales() { + mangoOutputsProcess.running = true + } + + // Query keyboard layout + function queryKeyboardLayout() { + mangoKeyboardProcess.running = true + } + + // Mango outputs process for display scale detection + Process { + id: mangoOutputsProcess + running: false + command: ["mmsg", "-g", "-A"] + + stdout: SplitParser { + onRead: function (line) { + try { + const parts = line.trim().split(/\s+/) + if (parts.length >= 3 && parts[1] === "scale_factor") { + const outputName = parts[0] + const scaleFactor = parseFloat(parts[2]) + + if (!monitorCache[outputName]) { + monitorCache[outputName] = {} + } + + monitorCache[outputName].scale = scaleFactor + monitorCache[outputName].name = outputName + } + } catch (e) { + Logger.e("MangoService", "Failed to parse output scale:", e, line) + } + } + } + + onExited: function (exitCode) { + if (exitCode !== 0) { + Logger.e("MangoService", "Failed to query outputs, exit code:", exitCode) + return + } + + // Convert to expected format and notify + const scales = {} + for (const [outputName, data] of Object.entries(monitorCache)) { + scales[outputName] = { + "name": data.name, + "scale": data.scale || 1.0, + "width": data.width || 0, + "height": data.height || 0, + "refresh_rate": data.refresh_rate || 0, + "x": data.x || 0, + "y": data.y || 0, + "active": data.active || false, + "focused": data.focused || false + } + } + + // Notify CompositorService + if (CompositorService && CompositorService.onDisplayScalesUpdated) { + CompositorService.onDisplayScalesUpdated(scales) + } + } + } + + // Mango tags process (workspaces) + Process { + id: mangoTagsProcess + running: false + command: ["mmsg", "-g", "-t"] + + property string accumulatedOutput: "" + + stdout: SplitParser { + onRead: function (line) { + mangoTagsProcess.accumulatedOutput += line + "\n" + } + } + + onExited: function (exitCode) { + if (exitCode !== 0) { + Logger.e("MangoService", "Failed to query tags, exit code:", exitCode) + accumulatedOutput = "" + return + } + + try { + parseTagsData(accumulatedOutput) + } catch (e) { + Logger.e("MangoService", "Failed to parse tags:", e) + } finally { + accumulatedOutput = "" + } + } + } + + // Mango windows process + Process { + id: mangoWindowsProcess + running: false + command: ["mmsg", "-g", "-c"] + + property string accumulatedOutput: "" + property var currentWindow: ({}) + + onRunningChanged: { + if (running) { + currentWindow = {} + } + } + + stdout: SplitParser { + onRead: function (line) { + const trimmed = line.trim() + if (!trimmed) return + + // Format: output property value + // Example: eDP-1 title joyous-triceratops | ~/.config/quickshell> y + // Example: eDP-1 appid com.mitchellh.ghostty + const firstSpace = trimmed.indexOf(' ') + if (firstSpace === -1) return + + const outputName = trimmed.substring(0, firstSpace) + const rest = trimmed.substring(firstSpace + 1).trim() + + const secondSpace = rest.indexOf(' ') + if (secondSpace === -1) return + + const property = rest.substring(0, secondSpace) + const value = rest.substring(secondSpace + 1).trim() + + + + if (!mangoWindowsProcess.currentWindow[outputName]) { + mangoWindowsProcess.currentWindow[outputName] = {} + } + + if (property === "title") { + mangoWindowsProcess.currentWindow[outputName].title = value + } else if (property === "appid") { + mangoWindowsProcess.currentWindow[outputName].appId = value + } else if (property === "fullscreen") { + mangoWindowsProcess.currentWindow[outputName].isFullscreen = value === "1" + } else if (property === "floating") { + mangoWindowsProcess.currentWindow[outputName].isFloating = value === "1" + } else if (property === "x") { + mangoWindowsProcess.currentWindow[outputName].x = parseInt(value) + } else if (property === "y") { + mangoWindowsProcess.currentWindow[outputName].y = parseInt(value) + } else if (property === "width") { + mangoWindowsProcess.currentWindow[outputName].width = parseInt(value) + } else if (property === "height") { + mangoWindowsProcess.currentWindow[outputName].height = parseInt(value) + } + } + } + + onExited: function (exitCode) { + if (exitCode !== 0) { + Logger.e("MangoService", "Failed to query windows, exit code:", exitCode) + accumulatedOutput = "" + currentWindow = {} + return + } + + try { + parseWindowsData(currentWindow) + } catch (e) { + Logger.e("MangoService", "Failed to parse windows:", e) + } finally { + currentWindow = {} + } + } + } + + // Mango keyboard layout process + Process { + id: mangoKeyboardProcess + running: false + command: ["mmsg", "-g", "-k"] + + stdout: SplitParser { + onRead: function (line) { + try { + const parts = line.trim().split(/\s+/) + if (parts.length >= 2 && parts[1] === "kb_layout") { + const layoutName = parts.slice(2).join(' ') + if (layoutName && layoutName !== currentKeyboardLayout) { + currentKeyboardLayout = layoutName + KeyboardLayoutService.setCurrentLayout(layoutName) + + } + } + } catch (e) { + Logger.e("MangoService", "Failed to parse keyboard layout:", e, line) + } + } + } + + onExited: function (exitCode) { + if (exitCode !== 0) { + Logger.e("MangoService", "Failed to query keyboard layout, exit code:", exitCode) + } + } + } + + + + // Mango event stream process + Process { + id: mangoEventStream + running: false + command: ["mmsg", "-w"] + + stdout: SplitParser { + onRead: function (line) { + try { + handleEvent(line.trim()) + } catch (e) { + Logger.e("MangoService", "Error parsing event:", e, line) + } + } + } + + onExited: function (exitCode) { + if (exitCode !== 0) { + Logger.e("MangoService", "Event stream exited, exit code:", exitCode) + // Restart event stream after a delay + restartTimer.start() + } + } + } + + // Timer to restart event stream + Timer { + id: restartTimer + interval: 1000 + onTriggered: { + if (initialized) { + mangoEventStream.running = true + } + } + } + + // Parse tags data and convert to workspace format + function parseTagsData(output) { + const lines = output.trim().split('\n') + const workspacesList = [] + const outputTags = {} + tagCache = {} + + for (const line of lines) { + const trimmed = line.trim() + if (!trimmed) continue + + // Parse tag information + // Format: output tag + // Example: eDP-1 tag 1 1 2 1 + const tagMatch = trimmed.match(/^(\S+)\s+tag\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$/) + if (tagMatch) { + const [, outputName, tagNum, state, clients, focused] = tagMatch + const tagId = parseInt(tagNum) + + // Store tag info per output + if (!outputTags[outputName]) { + outputTags[outputName] = [] + } + + // Convert MangoWC tag state to workspace properties + // Based on dwl-ipc protocol: bit 0=active, bit 1=urgent, bit 2=occupied + const isActive = (parseInt(state) & 1) !== 0 + const isUrgent = (parseInt(state) & 2) !== 0 // Note: might be bit 1 for urgent + const isOccupied = parseInt(clients) > 0 + + const workspaceData = { + "id": tagId, + "idx": tagId, + "name": tagId.toString(), + "output": outputName, + "isActive": isActive, + "isFocused": isActive && parseInt(focused) === 1, + "isUrgent": isUrgent, + "isOccupied": isOccupied + } + + tagCache[tagId] = workspaceData + outputTags[outputName].push(workspaceData) + + } + + // Parse layout information + // Format: output layout + const layoutMatch = trimmed.match(/^(\S+)\s+layout\s+(\S+)$/) + if (layoutMatch) { + const [, outputName, layoutName] = layoutMatch + currentLayout = layoutName + + // Detect overview state - MangoWC uses "󰃇" symbol for overview + const wasOverviewActive = overviewActive + overviewActive = (layoutName === "󰃇") + + // Emit signal if overview state changed + if (wasOverviewActive !== overviewActive) { + Logger.d("MangoService", `Overview state changed: ${overviewActive}`) + } + } + + // Parse clients count information + // Format: output clients + const clientsMatch = trimmed.match(/^(\S+)\s+clients\s+(\d+)$/) + if (clientsMatch) { + const [, outputName, clientsCount] = clientsMatch + // Store clients count for the output + if (!monitorCache[outputName]) { + monitorCache[outputName] = {} + } + monitorCache[outputName].clientsCount = parseInt(clientsCount) + } + + // Parse tags mask information + // Format: output tags + const tagsMaskMatch = trimmed.match(/^(\S+)\s+tags\s+(\d+)\s+(\d+)\s+(\d+)$/) + if (tagsMaskMatch) { + const [, outputName, occupiedMask, selectedMask, urgentMask] = tagsMaskMatch + if (!monitorCache[outputName]) { + monitorCache[outputName] = {} + } + monitorCache[outputName].occupiedMask = parseInt(occupiedMask) + monitorCache[outputName].selectedMask = parseInt(selectedMask) + monitorCache[outputName].urgentMask = parseInt(urgentMask) + } + } + + // Flatten all tags from all outputs into a single list + for (const [outputName, tags] of Object.entries(outputTags)) { + for (const tag of tags) { + workspacesList.push(tag) + } + } + + // Sort workspaces by tag ID, then by output name + workspacesList.sort((a, b) => { + if (a.id !== b.id) { + return a.id - b.id + } + return a.output.localeCompare(b.output) + }) + + // Update workspaces ListModel + workspaces.clear() + for (var i = 0; i < workspacesList.length; i++) { + workspaces.append(workspacesList[i]) + } + + workspaceChanged() + } + + // Parse windows data + function parseWindowsData(windowData) { + + const windowsList = [] + windowCache = {} + let newFocusedIndex = -1 + + for (const [outputName, data] of Object.entries(windowData)) { + if (data.title || data.appId) { + const windowInfo = { + "id": outputName, // Use output name as unique identifier for now + "title": data.title || "", + "appId": data.appId || "", + "workspaceId": getCurrentTagId(), // Get current active tag + "isFocused": false, // Will be determined by focused window detection + "output": outputName, + "class": data.appId || "", + "fullscreen": data.isFullscreen || false, + "floating": data.isFloating || false, + "x": data.x || 0, + "y": data.y || 0, + "width": data.width || 0, + "height": data.height || 0 + } + + windowsList.push(windowInfo) + windowCache[outputName] = windowInfo + } + } + + // Try to determine focused window by checking which output is focused + // This is a heuristic approach since mmsg doesn't provide direct focus info + for (let i = 0; i < windowsList.length; i++) { + const window = windowsList[i] + const outputData = monitorCache[window.output] + if (outputData && outputData.focused) { + window.isFocused = true + newFocusedIndex = i + break + } + } + + // Fallback: assume first window is focused if no output focus info + if (newFocusedIndex === -1 && windowsList.length > 0) { + windowsList[0].isFocused = true + newFocusedIndex = 0 + } + + windows = windowsList + + if (newFocusedIndex !== focusedWindowIndex) { + focusedWindowIndex = newFocusedIndex + activeWindowChanged() + } + + windowListChanged() + } + + // Get current active tag ID + function getCurrentTagId() { + for (const [tagId, tagData] of Object.entries(tagCache)) { + if (tagData.isActive) { + return parseInt(tagId) + } + } + return 1 // Default to tag 1 + } + + // Handle events from mmsg -w + function handleEvent(eventLine) { + const parts = eventLine.trim().split(/\s+/) + if (parts.length < 2) return + + const outputName = parts[0] + const eventType = parts[1] + + // Handle different event types + switch (eventType) { + case "tag": + // Tag state changed + updateTimer.restart() + break + case "title": + case "appid": + case "fullscreen": + case "floating": + // Window properties changed + updateTimer.restart() + break + case "layout": + // Layout changed + updateTimer.restart() + break + case "kb_layout": + // Keyboard layout changed + const layoutName = parts.slice(2).join(' ') + if (layoutName && layoutName !== currentKeyboardLayout) { + currentKeyboardLayout = layoutName + KeyboardLayoutService.setCurrentLayout(layoutName) + } + break + case "scale_factor": + // Display scale changed + queryDisplayScales() + break + case "monitor": + // Monitor configuration changed + queryDisplayScales() + updateTimer.restart() + break + case "client": + // Client (window) focus or state changed + updateTimer.restart() + break + case "selmon": + // Selected monitor changed + updateTimer.restart() + break + default: + // Unknown event type, trigger general update + + updateTimer.restart() + break + } + } + + // Safe update wrapper + function safeUpdate() { + safeUpdateWindows() + safeUpdateWorkspaces() + windowListChanged() + } + + // Safe workspace update + function safeUpdateWorkspaces() { + try { + updateWorkspaces() + } catch (e) { + Logger.e("MangoService", "Error updating workspaces:", e) + } + } + + // Safe window update + function safeUpdateWindows() { + try { + updateWindows() + } catch (e) { + Logger.e("MangoService", "Error updating windows:", e) + } + } + + // Public functions + function switchToWorkspace(workspace) { + try { + // MangoWC uses tags 1-9, so switch to tag by ID + const tagId = workspace.idx || workspace.id || 1 + Quickshell.execDetached(["mmsg", "-d", "view", tagId.toString()]) + } catch (e) { + Logger.e("MangoService", "Failed to switch workspace:", e) + } + } + + function focusWindow(window) { + try { + // For MangoWC, we can try to focus windows by switching to their workspace + // and then using focus commands, or by cycling through windows + if (window && window.workspaceId) { + // First switch to the window's workspace/tag + Quickshell.execDetached(["mmsg", "-d", "view", window.workspaceId.toString()]) + + // Then try to focus the window by cycling or using window-specific commands + // This is a limitation of the mmsg interface - we can't directly focus by window ID + Qt.callLater(() => { + // Give the workspace switch a moment to complete, then try to find the window + // For now, we'll use a generic focus command that focuses the main window + Quickshell.execDetached(["mmsg", "-d", "focusmaster"]) + }) + } + } catch (e) { + Logger.e("MangoService", "Failed to focus window:", e) + } + } + + function closeWindow(window) { + try { + // Close focused window + Quickshell.execDetached(["mmsg", "-d", "killclient"]) + } catch (e) { + Logger.e("MangoService", "Failed to close window:", e) + } + } + + function logout() { + try { + Quickshell.execDetached(["mmsg", "-d", "quit"]) + } catch (e) { + Logger.e("MangoService", "Failed to logout:", e) + } + } +} \ No newline at end of file From 8ba0a0a51f9bd8b3f7da6988bbe8eafb3e70780e Mon Sep 17 00:00:00 2001 From: atheeq-rhxn Date: Thu, 13 Nov 2025 20:27:40 +0530 Subject: [PATCH 2/8] refactor: Improve MangoWC implementation --- Services/Compositor/MangoService.qml | 861 +++++++++++++-------------- 1 file changed, 410 insertions(+), 451 deletions(-) diff --git a/Services/Compositor/MangoService.qml b/Services/Compositor/MangoService.qml index 3c7967b4..b7c9c1c7 100644 --- a/Services/Compositor/MangoService.qml +++ b/Services/Compositor/MangoService.qml @@ -2,17 +2,17 @@ import QtQuick import Quickshell import Quickshell.Io import qs.Commons -import qs.Services.Keyboard +import qs.Services.UI Item { id: root - // Properties that match the facade interface + // Properties matching facade interface property ListModel workspaces: ListModel {} property var windows: [] property int focusedWindowIndex: -1 - // Signals that match the facade interface + // Signals matching facade interface signal workspaceChanged signal activeWindowChanged signal windowListChanged @@ -21,12 +21,36 @@ Item { // Mango-specific properties property bool initialized: false property bool overviewActive: false - property var tagCache: ({}) + property var workspaceCache: ({}) property var windowCache: ({}) property var monitorCache: ({}) property string currentLayout: "" + property string currentLayoutSymbol: "" property string currentKeyboardLayout: "" + // Constants + readonly property var mmsgCommands: ({ + query: { + workspaces: ["mmsg", "-g", "-t"], + windows: ["mmsg", "-g", "-c"], + layout: ["mmsg", "-g", "-l"], + keyboard: ["mmsg", "-g", "-k"], + outputs: ["mmsg", "-g", "-A"], + eventStream: ["mmsg", "-w"] + }, + action: { + view: ["mmsg", "-d", "view"], + focusMaster: ["mmsg", "-d", "focusmaster"], + killClient: ["mmsg", "-d", "killclient"], + toggleOverview: ["mmsg", "-d", "toggleoverview"], + setLayout: ["mmsg", "-d", "setlayout"], + quit: ["mmsg", "-d", "quit"] + } + }) + + readonly property string overviewLayoutSymbol: "󰃇" + readonly property int defaultWorkspaceId: 1 + // Debounce timer for updates Timer { id: updateTimer @@ -35,53 +59,195 @@ Item { onTriggered: safeUpdate() } - // Initialization - function initialize() { - if (initialized) - return + // Event stream for real-time updates + Process { + id: eventStream + running: false + command: mmsgCommands.query.eventStream - try { - // Initial data fetch - updateWorkspaces() - updateWindows() - queryDisplayScales() - queryKeyboardLayout() - - // Start event watching - mangoEventStream.running = true - - initialized = true - Logger.i("MangoService", "Service started") - } catch (e) { - Logger.e("MangoService", "Failed to initialize:", e) + stdout: SplitParser { + onRead: function (line) { + try { + handleEvent(line.trim()) + } catch (e) { + Logger.e("MangoService", "Event parsing error:", e, line) + } + } + } + + onExited: function (exitCode) { + if (exitCode !== 0) { + Logger.e("MangoService", "Event stream exited, restarting...") + restartTimer.start() + } } } - // Update workspaces (tags in MangoWC) - function updateWorkspaces() { - mangoTagsProcess.running = true + Timer { + id: restartTimer + interval: 1000 + onTriggered: { + if (initialized) { + eventStream.running = true + } + } } - // Update windows - function updateWindows() { - mangoWindowsProcess.running = true - } - - // Query display scales - function queryDisplayScales() { - mangoOutputsProcess.running = true - } - - // Query keyboard layout - function queryKeyboardLayout() { - mangoKeyboardProcess.running = true - } - - // Mango outputs process for display scale detection + // Query processes Process { - id: mangoOutputsProcess + id: workspacesProcess running: false - command: ["mmsg", "-g", "-A"] + command: mmsgCommands.query.workspaces + property string accumulatedOutput: "" + + stdout: SplitParser { + onRead: function (line) { + workspacesProcess.accumulatedOutput += line + "\n" + } + } + + onExited: function (exitCode) { + if (exitCode === 0) { + parseWorkspaces(accumulatedOutput) + } else { + Logger.e("MangoService", "Workspaces query failed:", exitCode) + } + accumulatedOutput = "" + } + } + + Process { + id: windowsProcess + running: false + command: mmsgCommands.query.windows + property string accumulatedOutput: "" + property var currentWindow: ({}) + + onRunningChanged: { + if (running) { + currentWindow = {} + } + } + + stdout: SplitParser { + onRead: function (line) { + const trimmed = line.trim() + if (!trimmed) return + + const parts = trimmed.split(' ') + if (parts.length >= 3) { + const outputName = parts[0] + const property = parts[1] + const value = parts.slice(2).join(' ') + + if (!currentWindow[outputName]) { + currentWindow[outputName] = { + id: outputName, + output: outputName + } + } + + switch (property) { + case "title": + currentWindow[outputName].title = value + break + case "appid": + currentWindow[outputName].appId = value + currentWindow[outputName].class = value + break + case "fullscreen": + currentWindow[outputName].fullscreen = (value === "1") + break + case "floating": + currentWindow[outputName].floating = (value === "1") + break + case "x": + currentWindow[outputName].x = parseInt(value) + break + case "y": + currentWindow[outputName].y = parseInt(value) + break + case "width": + currentWindow[outputName].width = parseInt(value) + break + case "height": + currentWindow[outputName].height = parseInt(value) + break + } + } + } + } + + onExited: function (exitCode) { + if (exitCode === 0) { + parseWindows(currentWindow) + } else { + Logger.e("MangoService", "Windows query failed:", exitCode) + } + accumulatedOutput = "" + currentWindow = {} + } + } + + Process { + id: layoutProcess + running: false + command: mmsgCommands.query.layout + + stdout: SplitParser { + onRead: function (line) { + try { + const parts = line.trim().split(/\s+/) + if (parts.length >= 2) { + const layoutSymbol = parts.slice(1).join(' ') + handleLayoutChange(layoutSymbol) + } + } catch (e) { + Logger.e("MangoService", "Layout parsing error:", e, line) + } + } + } + + onExited: function (exitCode) { + if (exitCode !== 0) { + Logger.e("MangoService", "Layout query failed:", exitCode) + } + } + } + + Process { + id: keyboardProcess + running: false + command: mmsgCommands.query.keyboard + + stdout: SplitParser { + onRead: function (line) { + try { + const parts = line.trim().split(/\s+/) + if (parts.length >= 2 && parts[1] === "kb_layout") { + const layoutName = parts.slice(2).join(' ') + if (layoutName && layoutName !== currentKeyboardLayout) { + currentKeyboardLayout = layoutName + KeyboardLayoutService.setCurrentLayout(layoutName) + } + } + } catch (e) { + Logger.e("MangoService", "Keyboard layout parsing error:", e, line) + } + } + } + + onExited: function (exitCode) { + if (exitCode !== 0) { + Logger.e("MangoService", "Keyboard query failed:", exitCode) + } + } + } + + Process { + id: outputsProcess + running: false + command: mmsgCommands.query.outputs stdout: SplitParser { onRead: function (line) { @@ -99,364 +265,201 @@ Item { monitorCache[outputName].name = outputName } } catch (e) { - Logger.e("MangoService", "Failed to parse output scale:", e, line) + Logger.e("MangoService", "Output parsing error:", e, line) } } } onExited: function (exitCode) { - if (exitCode !== 0) { - Logger.e("MangoService", "Failed to query outputs, exit code:", exitCode) - return - } - - // Convert to expected format and notify - const scales = {} - for (const [outputName, data] of Object.entries(monitorCache)) { - scales[outputName] = { - "name": data.name, - "scale": data.scale || 1.0, - "width": data.width || 0, - "height": data.height || 0, - "refresh_rate": data.refresh_rate || 0, - "x": data.x || 0, - "y": data.y || 0, - "active": data.active || false, - "focused": data.focused || false - } - } - - // Notify CompositorService - if (CompositorService && CompositorService.onDisplayScalesUpdated) { - CompositorService.onDisplayScalesUpdated(scales) + if (exitCode === 0) { + updateDisplayScales() + } else { + Logger.e("MangoService", "Outputs query failed:", exitCode) } } } - // Mango tags process (workspaces) - Process { - id: mangoTagsProcess - running: false - command: ["mmsg", "-g", "-t"] - - property string accumulatedOutput: "" - - stdout: SplitParser { - onRead: function (line) { - mangoTagsProcess.accumulatedOutput += line + "\n" - } + // Initialization + function initialize() { + if (initialized) { + Logger.w("MangoService", "Already initialized") + return } - onExited: function (exitCode) { - if (exitCode !== 0) { - Logger.e("MangoService", "Failed to query tags, exit code:", exitCode) - accumulatedOutput = "" - return - } - - try { - parseTagsData(accumulatedOutput) - } catch (e) { - Logger.e("MangoService", "Failed to parse tags:", e) - } finally { - accumulatedOutput = "" - } + try { + Logger.i("MangoService", "Initializing MangoWC service...") + + eventStream.running = true + queryWorkspaces() + queryWindows() + queryLayout() + queryKeyboard() + queryOutputs() + + initialized = true + Logger.i("MangoService", "Service initialized successfully") + } catch (e) { + Logger.e("MangoService", "Initialization failed:", e) + eventStream.running = true } } - // Mango windows process - Process { - id: mangoWindowsProcess - running: false - command: ["mmsg", "-g", "-c"] - - property string accumulatedOutput: "" - property var currentWindow: ({}) - - onRunningChanged: { - if (running) { - currentWindow = {} - } + // Workspace operations + function switchToWorkspace(workspace) { + try { + const tagId = workspace.idx || workspace.id || defaultWorkspaceId + const command = mmsgCommands.action.view.concat([tagId.toString()]) + Quickshell.execDetached(command) + Logger.d("MangoService", `Switching to workspace ${tagId}`) + } catch (e) { + Logger.e("MangoService", "Failed to switch workspace:", e) } + } - stdout: SplitParser { - onRead: function (line) { - const trimmed = line.trim() - if (!trimmed) return - - // Format: output property value - // Example: eDP-1 title joyous-triceratops | ~/.config/quickshell> y - // Example: eDP-1 appid com.mitchellh.ghostty - const firstSpace = trimmed.indexOf(' ') - if (firstSpace === -1) return + // Window operations + function focusWindow(window) { + try { + if (window && window.workspaceId) { + const command = mmsgCommands.action.view.concat([window.workspaceId.toString()]) + Quickshell.execDetached(command) - const outputName = trimmed.substring(0, firstSpace) - const rest = trimmed.substring(firstSpace + 1).trim() - - const secondSpace = rest.indexOf(' ') - if (secondSpace === -1) return - - const property = rest.substring(0, secondSpace) - const value = rest.substring(secondSpace + 1).trim() - - - - if (!mangoWindowsProcess.currentWindow[outputName]) { - mangoWindowsProcess.currentWindow[outputName] = {} - } - - if (property === "title") { - mangoWindowsProcess.currentWindow[outputName].title = value - } else if (property === "appid") { - mangoWindowsProcess.currentWindow[outputName].appId = value - } else if (property === "fullscreen") { - mangoWindowsProcess.currentWindow[outputName].isFullscreen = value === "1" - } else if (property === "floating") { - mangoWindowsProcess.currentWindow[outputName].isFloating = value === "1" - } else if (property === "x") { - mangoWindowsProcess.currentWindow[outputName].x = parseInt(value) - } else if (property === "y") { - mangoWindowsProcess.currentWindow[outputName].y = parseInt(value) - } else if (property === "width") { - mangoWindowsProcess.currentWindow[outputName].width = parseInt(value) - } else if (property === "height") { - mangoWindowsProcess.currentWindow[outputName].height = parseInt(value) - } - } - } - - onExited: function (exitCode) { - if (exitCode !== 0) { - Logger.e("MangoService", "Failed to query windows, exit code:", exitCode) - accumulatedOutput = "" - currentWindow = {} - return - } - - try { - parseWindowsData(currentWindow) - } catch (e) { - Logger.e("MangoService", "Failed to parse windows:", e) - } finally { - currentWindow = {} + Qt.callLater(() => { + Quickshell.execDetached(mmsgCommands.action.focusMaster) + }) } + } catch (e) { + Logger.e("MangoService", "Failed to focus window:", e) } } - // Mango keyboard layout process - Process { - id: mangoKeyboardProcess - running: false - command: ["mmsg", "-g", "-k"] - - stdout: SplitParser { - onRead: function (line) { - try { - const parts = line.trim().split(/\s+/) - if (parts.length >= 2 && parts[1] === "kb_layout") { - const layoutName = parts.slice(2).join(' ') - if (layoutName && layoutName !== currentKeyboardLayout) { - currentKeyboardLayout = layoutName - KeyboardLayoutService.setCurrentLayout(layoutName) - - } - } - } catch (e) { - Logger.e("MangoService", "Failed to parse keyboard layout:", e, line) - } - } - } - - onExited: function (exitCode) { - if (exitCode !== 0) { - Logger.e("MangoService", "Failed to query keyboard layout, exit code:", exitCode) - } + function closeWindow() { + try { + Quickshell.execDetached(mmsgCommands.action.killClient) + } catch (e) { + Logger.e("MangoService", "Failed to close window:", e) } } - - - // Mango event stream process - Process { - id: mangoEventStream - running: false - command: ["mmsg", "-w"] - - stdout: SplitParser { - onRead: function (line) { - try { - handleEvent(line.trim()) - } catch (e) { - Logger.e("MangoService", "Error parsing event:", e, line) - } - } - } - - onExited: function (exitCode) { - if (exitCode !== 0) { - Logger.e("MangoService", "Event stream exited, exit code:", exitCode) - // Restart event stream after a delay - restartTimer.start() - } + // MangoWC-specific operations + function toggleOverview() { + try { + Quickshell.execDetached(mmsgCommands.action.toggleOverview) + } catch (e) { + Logger.e("MangoService", "Failed to toggle overview:", e) } } - // Timer to restart event stream - Timer { - id: restartTimer - interval: 1000 - onTriggered: { - if (initialized) { - mangoEventStream.running = true - } + function setLayout(layoutName) { + try { + const command = mmsgCommands.action.setLayout.concat([layoutName]) + Quickshell.execDetached(command) + } catch (e) { + Logger.e("MangoService", "Failed to set layout:", e) } } - // Parse tags data and convert to workspace format - function parseTagsData(output) { + function logout() { + try { + Quickshell.execDetached(mmsgCommands.action.quit) + } catch (e) { + Logger.e("MangoService", "Failed to logout:", e) + } + } + + // Data parsing + function parseWorkspaces(output) { const lines = output.trim().split('\n') const workspacesList = [] - const outputTags = {} - tagCache = {} + const newWorkspaceCache = {} for (const line of lines) { const trimmed = line.trim() if (!trimmed) continue - // Parse tag information - // Format: output tag - // Example: eDP-1 tag 1 1 2 1 - const tagMatch = trimmed.match(/^(\S+)\s+tag\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$/) - if (tagMatch) { - const [, outputName, tagNum, state, clients, focused] = tagMatch + const match = trimmed.match(/^(\S+)\s+tag\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$/) + if (match) { + const [, outputName, tagNum, state, clients, focused] = match const tagId = parseInt(tagNum) - // Store tag info per output - if (!outputTags[outputName]) { - outputTags[outputName] = [] - } - - // Convert MangoWC tag state to workspace properties - // Based on dwl-ipc protocol: bit 0=active, bit 1=urgent, bit 2=occupied const isActive = (parseInt(state) & 1) !== 0 - const isUrgent = (parseInt(state) & 2) !== 0 // Note: might be bit 1 for urgent + const isUrgent = (parseInt(state) & 2) !== 0 const isOccupied = parseInt(clients) > 0 + const isFocused = isActive && parseInt(focused) === 1 const workspaceData = { - "id": tagId, - "idx": tagId, - "name": tagId.toString(), - "output": outputName, - "isActive": isActive, - "isFocused": isActive && parseInt(focused) === 1, - "isUrgent": isUrgent, - "isOccupied": isOccupied + id: tagId, + idx: tagId, + name: tagId.toString(), + output: outputName, + isActive: isActive, + isFocused: isFocused, + isUrgent: isUrgent, + isOccupied: isOccupied, + clients: parseInt(clients) } - tagCache[tagId] = workspaceData - outputTags[outputName].push(workspaceData) - + newWorkspaceCache[tagId] = workspaceData + workspacesList.push(workspaceData) } - // Parse layout information - // Format: output layout const layoutMatch = trimmed.match(/^(\S+)\s+layout\s+(\S+)$/) if (layoutMatch) { - const [, outputName, layoutName] = layoutMatch - currentLayout = layoutName - - // Detect overview state - MangoWC uses "󰃇" symbol for overview - const wasOverviewActive = overviewActive - overviewActive = (layoutName === "󰃇") - - // Emit signal if overview state changed - if (wasOverviewActive !== overviewActive) { - Logger.d("MangoService", `Overview state changed: ${overviewActive}`) - } - } - - // Parse clients count information - // Format: output clients - const clientsMatch = trimmed.match(/^(\S+)\s+clients\s+(\d+)$/) - if (clientsMatch) { - const [, outputName, clientsCount] = clientsMatch - // Store clients count for the output - if (!monitorCache[outputName]) { - monitorCache[outputName] = {} - } - monitorCache[outputName].clientsCount = parseInt(clientsCount) - } - - // Parse tags mask information - // Format: output tags - const tagsMaskMatch = trimmed.match(/^(\S+)\s+tags\s+(\d+)\s+(\d+)\s+(\d+)$/) - if (tagsMaskMatch) { - const [, outputName, occupiedMask, selectedMask, urgentMask] = tagsMaskMatch - if (!monitorCache[outputName]) { - monitorCache[outputName] = {} - } - monitorCache[outputName].occupiedMask = parseInt(occupiedMask) - monitorCache[outputName].selectedMask = parseInt(selectedMask) - monitorCache[outputName].urgentMask = parseInt(urgentMask) + const [, , layoutSymbol] = layoutMatch + handleLayoutChange(layoutSymbol) } } - // Flatten all tags from all outputs into a single list - for (const [outputName, tags] of Object.entries(outputTags)) { - for (const tag of tags) { - workspacesList.push(tag) + if (JSON.stringify(newWorkspaceCache) !== JSON.stringify(workspaceCache)) { + workspaceCache = newWorkspaceCache + + workspacesList.sort((a, b) => { + if (a.id !== b.id) return a.id - b.id + return a.output.localeCompare(b.output) + }) + + workspaces.clear() + for (var i = 0; i < workspacesList.length; i++) { + workspaces.append(workspacesList[i]) } + + workspaceChanged() } - - // Sort workspaces by tag ID, then by output name - workspacesList.sort((a, b) => { - if (a.id !== b.id) { - return a.id - b.id - } - return a.output.localeCompare(b.output) - }) - - // Update workspaces ListModel - workspaces.clear() - for (var i = 0; i < workspacesList.length; i++) { - workspaces.append(workspacesList[i]) - } - - workspaceChanged() } - // Parse windows data - function parseWindowsData(windowData) { - + function parseWindows(windowData) { const windowsList = [] - windowCache = {} + const newWindowCache = {} let newFocusedIndex = -1 for (const [outputName, data] of Object.entries(windowData)) { if (data.title || data.appId) { const windowInfo = { - "id": outputName, // Use output name as unique identifier for now - "title": data.title || "", - "appId": data.appId || "", - "workspaceId": getCurrentTagId(), // Get current active tag - "isFocused": false, // Will be determined by focused window detection - "output": outputName, - "class": data.appId || "", - "fullscreen": data.isFullscreen || false, - "floating": data.isFloating || false, - "x": data.x || 0, - "y": data.y || 0, - "width": data.width || 0, - "height": data.height || 0 + id: outputName, + title: data.title || "", + appId: data.appId || "", + class: data.appId || "", + workspaceId: getCurrentActiveTagId(), + isFocused: false, + output: outputName, + fullscreen: data.fullscreen || false, + floating: data.floating || false, + x: data.x || 0, + y: data.y || 0, + width: data.width || 0, + height: data.height || 0, + geometry: { + x: data.x || 0, + y: data.y || 0, + width: data.width || 0, + height: data.height || 0 + } } windowsList.push(windowInfo) - windowCache[outputName] = windowInfo + newWindowCache[outputName] = windowInfo } } - // Try to determine focused window by checking which output is focused - // This is a heuristic approach since mmsg doesn't provide direct focus info for (let i = 0; i < windowsList.length; i++) { const window = windowsList[i] const outputData = monitorCache[window.output] @@ -467,161 +470,117 @@ Item { } } - // Fallback: assume first window is focused if no output focus info - if (newFocusedIndex === -1 && windowsList.length > 0) { - windowsList[0].isFocused = true - newFocusedIndex = 0 + if (JSON.stringify(newWindowCache) !== JSON.stringify(windowCache)) { + windowCache = newWindowCache + windows = windowsList + + if (newFocusedIndex !== focusedWindowIndex) { + focusedWindowIndex = newFocusedIndex + activeWindowChanged() + } + + windowListChanged() } - - windows = windowsList - - if (newFocusedIndex !== focusedWindowIndex) { - focusedWindowIndex = newFocusedIndex - activeWindowChanged() - } - - windowListChanged() } - // Get current active tag ID - function getCurrentTagId() { - for (const [tagId, tagData] of Object.entries(tagCache)) { - if (tagData.isActive) { - return parseInt(tagId) + function handleLayoutChange(layoutSymbol) { + const wasOverview = overviewActive + const isOverview = (layoutSymbol === overviewLayoutSymbol) + + if (wasOverview !== isOverview) { + overviewActive = isOverview + Logger.d("MangoService", `Overview mode: ${overviewActive}`) + } + + if (layoutSymbol !== currentLayoutSymbol) { + currentLayoutSymbol = layoutSymbol + currentLayout = layoutSymbol + } + } + + function updateDisplayScales() { + const scales = {} + for (const [outputName, data] of Object.entries(monitorCache)) { + scales[outputName] = { + name: data.name || outputName, + scale: data.scale || 1.0, + width: data.width || 0, + height: data.height || 0, + refresh_rate: data.refresh_rate || 0, + x: data.x || 0, + y: data.y || 0, + active: data.active || false, + focused: data.focused || false } } - return 1 // Default to tag 1 + + if (CompositorService && CompositorService.onDisplayScalesUpdated) { + CompositorService.onDisplayScalesUpdated(scales) + } + displayScalesChanged() } - // Handle events from mmsg -w + // Event handling function handleEvent(eventLine) { const parts = eventLine.trim().split(/\s+/) if (parts.length < 2) return - const outputName = parts[0] const eventType = parts[1] - // Handle different event types switch (eventType) { case "tag": - // Tag state changed - updateTimer.restart() - break case "title": case "appid": case "fullscreen": case "floating": - // Window properties changed - updateTimer.restart() - break case "layout": - // Layout changed - updateTimer.restart() - break case "kb_layout": - // Keyboard layout changed - const layoutName = parts.slice(2).join(' ') - if (layoutName && layoutName !== currentKeyboardLayout) { - currentKeyboardLayout = layoutName - KeyboardLayoutService.setCurrentLayout(layoutName) - } - break case "scale_factor": - // Display scale changed - queryDisplayScales() - break case "monitor": - // Monitor configuration changed - queryDisplayScales() - updateTimer.restart() - break case "client": - // Client (window) focus or state changed - updateTimer.restart() - break case "selmon": - // Selected monitor changed - updateTimer.restart() - break - default: - // Unknown event type, trigger general update - updateTimer.restart() break } } - // Safe update wrapper + // Queries + function queryWorkspaces() { + workspacesProcess.running = true + } + + function queryWindows() { + windowsProcess.running = true + } + + function queryLayout() { + layoutProcess.running = true + } + + function queryKeyboard() { + keyboardProcess.running = true + } + + function queryOutputs() { + outputsProcess.running = true + } + + // Utilities function safeUpdate() { - safeUpdateWindows() - safeUpdateWorkspaces() - windowListChanged() - } - - // Safe workspace update - function safeUpdateWorkspaces() { try { - updateWorkspaces() + queryWorkspaces() + queryWindows() } catch (e) { - Logger.e("MangoService", "Error updating workspaces:", e) + Logger.e("MangoService", "Safe update failed:", e) } } - // Safe window update - function safeUpdateWindows() { - try { - updateWindows() - } catch (e) { - Logger.e("MangoService", "Error updating windows:", e) - } - } - - // Public functions - function switchToWorkspace(workspace) { - try { - // MangoWC uses tags 1-9, so switch to tag by ID - const tagId = workspace.idx || workspace.id || 1 - Quickshell.execDetached(["mmsg", "-d", "view", tagId.toString()]) - } catch (e) { - Logger.e("MangoService", "Failed to switch workspace:", e) - } - } - - function focusWindow(window) { - try { - // For MangoWC, we can try to focus windows by switching to their workspace - // and then using focus commands, or by cycling through windows - if (window && window.workspaceId) { - // First switch to the window's workspace/tag - Quickshell.execDetached(["mmsg", "-d", "view", window.workspaceId.toString()]) - - // Then try to focus the window by cycling or using window-specific commands - // This is a limitation of the mmsg interface - we can't directly focus by window ID - Qt.callLater(() => { - // Give the workspace switch a moment to complete, then try to find the window - // For now, we'll use a generic focus command that focuses the main window - Quickshell.execDetached(["mmsg", "-d", "focusmaster"]) - }) + function getCurrentActiveTagId() { + for (const [tagId, tagData] of Object.entries(workspaceCache)) { + if (tagData.isActive) { + return parseInt(tagId) } - } catch (e) { - Logger.e("MangoService", "Failed to focus window:", e) - } - } - - function closeWindow(window) { - try { - // Close focused window - Quickshell.execDetached(["mmsg", "-d", "killclient"]) - } catch (e) { - Logger.e("MangoService", "Failed to close window:", e) - } - } - - function logout() { - try { - Quickshell.execDetached(["mmsg", "-d", "quit"]) - } catch (e) { - Logger.e("MangoService", "Failed to logout:", e) } + return defaultWorkspaceId } } \ No newline at end of file From 3a80389ca487dd7464e7b93db5942ce81ced9b18 Mon Sep 17 00:00:00 2001 From: atheeq-rhxn Date: Thu, 13 Nov 2025 21:21:15 +0530 Subject: [PATCH 3/8] fix(workspace): use tag command instead of view to prevent window moving --- Services/Compositor/MangoService.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Services/Compositor/MangoService.qml b/Services/Compositor/MangoService.qml index b7c9c1c7..22e8349e 100644 --- a/Services/Compositor/MangoService.qml +++ b/Services/Compositor/MangoService.qml @@ -40,6 +40,7 @@ Item { }, action: { view: ["mmsg", "-d", "view"], + tag: ["mmsg", "-t"], focusMaster: ["mmsg", "-d", "focusmaster"], killClient: ["mmsg", "-d", "killclient"], toggleOverview: ["mmsg", "-d", "toggleoverview"], @@ -308,7 +309,7 @@ Item { function switchToWorkspace(workspace) { try { const tagId = workspace.idx || workspace.id || defaultWorkspaceId - const command = mmsgCommands.action.view.concat([tagId.toString()]) + const command = mmsgCommands.action.tag.concat([tagId.toString()]) Quickshell.execDetached(command) Logger.d("MangoService", `Switching to workspace ${tagId}`) } catch (e) { From a49f4ba009d334ac0689a2e925cdbd42aa2cd259 Mon Sep 17 00:00:00 2001 From: atheeq-rhxn Date: Thu, 13 Nov 2025 22:06:06 +0530 Subject: [PATCH 4/8] fix: resolve MangoService window parsing scope and add KeyboardLayoutService import --- Services/Compositor/MangoService.qml | 29 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/Services/Compositor/MangoService.qml b/Services/Compositor/MangoService.qml index 22e8349e..18c6b058 100644 --- a/Services/Compositor/MangoService.qml +++ b/Services/Compositor/MangoService.qml @@ -3,6 +3,7 @@ import Quickshell import Quickshell.Io import qs.Commons import qs.Services.UI +import qs.Services.Keyboard Item { id: root @@ -126,7 +127,7 @@ Item { onRunningChanged: { if (running) { - currentWindow = {} + windowsProcess.currentWindow = {} } } @@ -141,8 +142,8 @@ Item { const property = parts[1] const value = parts.slice(2).join(' ') - if (!currentWindow[outputName]) { - currentWindow[outputName] = { + if (!windowsProcess.currentWindow[outputName]) { + windowsProcess.currentWindow[outputName] = { id: outputName, output: outputName } @@ -150,29 +151,29 @@ Item { switch (property) { case "title": - currentWindow[outputName].title = value + windowsProcess.currentWindow[outputName].title = value break case "appid": - currentWindow[outputName].appId = value - currentWindow[outputName].class = value + windowsProcess.currentWindow[outputName].appId = value + windowsProcess.currentWindow[outputName].class = value break case "fullscreen": - currentWindow[outputName].fullscreen = (value === "1") + windowsProcess.currentWindow[outputName].fullscreen = (value === "1") break case "floating": - currentWindow[outputName].floating = (value === "1") + windowsProcess.currentWindow[outputName].floating = (value === "1") break case "x": - currentWindow[outputName].x = parseInt(value) + windowsProcess.currentWindow[outputName].x = parseInt(value) break case "y": - currentWindow[outputName].y = parseInt(value) + windowsProcess.currentWindow[outputName].y = parseInt(value) break case "width": - currentWindow[outputName].width = parseInt(value) + windowsProcess.currentWindow[outputName].width = parseInt(value) break case "height": - currentWindow[outputName].height = parseInt(value) + windowsProcess.currentWindow[outputName].height = parseInt(value) break } } @@ -181,12 +182,12 @@ Item { onExited: function (exitCode) { if (exitCode === 0) { - parseWindows(currentWindow) + parseWindows(windowsProcess.currentWindow) } else { Logger.e("MangoService", "Windows query failed:", exitCode) } accumulatedOutput = "" - currentWindow = {} + windowsProcess.currentWindow = {} } } From 06007549a34e3d70ba52f9762661bf4e87a3ee68 Mon Sep 17 00:00:00 2001 From: atheeq-rhxn Date: Thu, 13 Nov 2025 22:56:23 +0530 Subject: [PATCH 5/8] fix: resolve active window detection --- Services/Compositor/MangoService.qml | 72 +++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/Services/Compositor/MangoService.qml b/Services/Compositor/MangoService.qml index 18c6b058..3379a925 100644 --- a/Services/Compositor/MangoService.qml +++ b/Services/Compositor/MangoService.qml @@ -28,6 +28,7 @@ Item { property string currentLayout: "" property string currentLayoutSymbol: "" property string currentKeyboardLayout: "" + property string selectedMonitor: "" // Constants readonly property var mmsgCommands: ({ @@ -281,6 +282,36 @@ Item { } } + Process { + id: monitorStateProcess + running: false + command: ["mmsg", "-g"] + + stdout: SplitParser { + onRead: function (line) { + try { + const parts = line.trim().split(/\s+/) + if (parts.length >= 3 && parts[1] === "selmon") { + const outputName = parts[0] + const isSelected = parts[2] === "1" + if (isSelected) { + selectedMonitor = outputName + Logger.d("MangoService", `Initial selected monitor: ${outputName}`) + } + } + } catch (e) { + Logger.e("MangoService", "Monitor state parsing error:", e, line) + } + } + } + + onExited: function (exitCode) { + if (exitCode !== 0) { + Logger.e("MangoService", "Monitor state query failed:", exitCode) + } + } + } + // Initialization function initialize() { if (initialized) { @@ -291,6 +322,8 @@ Item { try { Logger.i("MangoService", "Initializing MangoWC service...") + // Query monitor state first to establish selected monitor before parsing windows + queryMonitorState() eventStream.running = true queryWorkspaces() queryWindows() @@ -435,13 +468,18 @@ Item { for (const [outputName, data] of Object.entries(windowData)) { if (data.title || data.appId) { + // Windows from mmsg -g -c are already the focused windows for their respective outputs + // A window is focused if it's from the currently selected monitor + // If selectedMonitor is not yet set, assume first window is focused (fallback) + const isFocused = selectedMonitor ? (outputName === selectedMonitor) : (windowsList.length === 0) + const windowInfo = { id: outputName, title: data.title || "", appId: data.appId || "", class: data.appId || "", workspaceId: getCurrentActiveTagId(), - isFocused: false, + isFocused: isFocused, output: outputName, fullscreen: data.fullscreen || false, floating: data.floating || false, @@ -459,16 +497,11 @@ Item { windowsList.push(windowInfo) newWindowCache[outputName] = windowInfo - } - } - - for (let i = 0; i < windowsList.length; i++) { - const window = windowsList[i] - const outputData = monitorCache[window.output] - if (outputData && outputData.focused) { - window.isFocused = true - newFocusedIndex = i - break + + if (isFocused) { + newFocusedIndex = windowsList.length - 1 + Logger.d("MangoService", `Focused window detected: ${data.title} on ${outputName}`) + } } } @@ -530,6 +563,17 @@ Item { const eventType = parts[1] switch (eventType) { + case "selmon": + if (parts.length >= 3) { + const monitorName = parts[0] + const isSelected = parts[2] === "1" + if (isSelected) { + selectedMonitor = monitorName + Logger.d("MangoService", `Selected monitor changed to: ${monitorName}`) + } + } + updateTimer.restart() + break case "tag": case "title": case "appid": @@ -540,7 +584,6 @@ Item { case "scale_factor": case "monitor": case "client": - case "selmon": updateTimer.restart() break } @@ -567,11 +610,16 @@ Item { outputsProcess.running = true } + function queryMonitorState() { + monitorStateProcess.running = true + } + // Utilities function safeUpdate() { try { queryWorkspaces() queryWindows() + queryMonitorState() } catch (e) { Logger.e("MangoService", "Safe update failed:", e) } From ee22bb9e21ebf67ef29bac88187556e8435208ab Mon Sep 17 00:00:00 2001 From: atheeq-rhxn Date: Fri, 14 Nov 2025 15:29:46 +0530 Subject: [PATCH 6/8] feat: add multi-monitor support --- Services/Compositor/MangoService.qml | 215 +++++++++++++++++++++------ 1 file changed, 169 insertions(+), 46 deletions(-) diff --git a/Services/Compositor/MangoService.qml b/Services/Compositor/MangoService.qml index 3379a925..64d0676c 100644 --- a/Services/Compositor/MangoService.qml +++ b/Services/Compositor/MangoService.qml @@ -38,16 +38,17 @@ Item { layout: ["mmsg", "-g", "-l"], keyboard: ["mmsg", "-g", "-k"], outputs: ["mmsg", "-g", "-A"], + monitors: ["mmsg", "-g", "-o"], eventStream: ["mmsg", "-w"] }, action: { - view: ["mmsg", "-d", "view"], - tag: ["mmsg", "-t"], - focusMaster: ["mmsg", "-d", "focusmaster"], - killClient: ["mmsg", "-d", "killclient"], - toggleOverview: ["mmsg", "-d", "toggleoverview"], - setLayout: ["mmsg", "-d", "setlayout"], - quit: ["mmsg", "-d", "quit"] + view: ["mmsg", "-s", "-d", "view"], + tag: ["mmsg", "-s", "-t"], + focusMaster: ["mmsg", "-s", "-d", "focusmaster"], + killClient: ["mmsg", "-s", "-d", "killclient"], + toggleOverview: ["mmsg", "-s", "-d", "toggleoverview"], + setLayout: ["mmsg", "-s", "-d", "setlayout"], + quit: ["mmsg", "-s", "-q"] } }) @@ -285,7 +286,7 @@ Item { Process { id: monitorStateProcess running: false - command: ["mmsg", "-g"] + command: mmsgCommands.query.monitors stdout: SplitParser { onRead: function (line) { @@ -312,6 +313,38 @@ Item { } } + Process { + id: outputEnumProcess + running: false + command: ["mmsg", "-g", "-O"] + + stdout: SplitParser { + onRead: function (line) { + try { + const trimmed = line.trim() + // Handle output enumeration format: "+ eDP-1" + const outputName = trimmed.replace(/^\+\s*/, '') + if (outputName && !monitorCache[outputName]) { + monitorCache[outputName] = { + name: outputName, + scale: 1.0, + active: false, + focused: false + } + } + } catch (e) { + Logger.e("MangoService", "Output enumeration error:", e, line) + } + } + } + + onExited: function (exitCode) { + if (exitCode !== 0) { + Logger.e("MangoService", "Output enumeration failed:", exitCode) + } + } + } + // Initialization function initialize() { if (initialized) { @@ -323,6 +356,7 @@ Item { Logger.i("MangoService", "Initializing MangoWC service...") // Query monitor state first to establish selected monitor before parsing windows + queryOutputEnum() queryMonitorState() eventStream.running = true queryWorkspaces() @@ -343,9 +377,16 @@ Item { function switchToWorkspace(workspace) { try { const tagId = workspace.idx || workspace.id || defaultWorkspaceId - const command = mmsgCommands.action.tag.concat([tagId.toString()]) + const outputName = workspace.output || selectedMonitor || "" + let command = [...mmsgCommands.action.tag] + + if (outputName) { + command.push("-o", outputName) + } + command.push(tagId.toString()) + Quickshell.execDetached(command) - Logger.d("MangoService", `Switching to workspace ${tagId}`) + Logger.d("MangoService", `Switching to workspace ${tagId} on ${outputName || 'default output'}`) } catch (e) { Logger.e("MangoService", "Failed to switch workspace:", e) } @@ -354,12 +395,15 @@ Item { // Window operations function focusWindow(window) { try { - if (window && window.workspaceId) { - const command = mmsgCommands.action.view.concat([window.workspaceId.toString()]) + if (window && window.output) { + let command = [...mmsgCommands.action.view] + command.push("-o", window.output, window.workspaceId.toString()) Quickshell.execDetached(command) Qt.callLater(() => { - Quickshell.execDetached(mmsgCommands.action.focusMaster) + let focusCommand = [...mmsgCommands.action.focusMaster] + focusCommand.push("-o", window.output) + Quickshell.execDetached(focusCommand) }) } } catch (e) { @@ -367,9 +411,13 @@ Item { } } - function closeWindow() { + function closeWindow(window) { try { - Quickshell.execDetached(mmsgCommands.action.killClient) + const command = [...mmsgCommands.action.killClient] + if (selectedMonitor) { + command.push("-o", selectedMonitor) + } + Quickshell.execDetached(command) } catch (e) { Logger.e("MangoService", "Failed to close window:", e) } @@ -378,7 +426,11 @@ Item { // MangoWC-specific operations function toggleOverview() { try { - Quickshell.execDetached(mmsgCommands.action.toggleOverview) + const command = [...mmsgCommands.action.toggleOverview] + if (selectedMonitor) { + command.push("-o", selectedMonitor) + } + Quickshell.execDetached(command) } catch (e) { Logger.e("MangoService", "Failed to toggle overview:", e) } @@ -386,7 +438,8 @@ Item { function setLayout(layoutName) { try { - const command = mmsgCommands.action.setLayout.concat([layoutName]) + const command = [...mmsgCommands.action.setLayout] + command.push(layoutName) Quickshell.execDetached(command) } catch (e) { Logger.e("MangoService", "Failed to set layout:", e) @@ -406,14 +459,15 @@ Item { const lines = output.trim().split('\n') const workspacesList = [] const newWorkspaceCache = {} + let outputClients = {} for (const line of lines) { const trimmed = line.trim() if (!trimmed) continue - const match = trimmed.match(/^(\S+)\s+tag\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$/) - if (match) { - const [, outputName, tagNum, state, clients, focused] = match + const tagMatch = trimmed.match(/^(\S+)\s+tag\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$/) + if (tagMatch) { + const [, outputName, tagNum, state, clients, focused] = tagMatch const tagId = parseInt(tagNum) const isActive = (parseInt(state) & 1) !== 0 @@ -421,22 +475,66 @@ Item { const isOccupied = parseInt(clients) > 0 const isFocused = isActive && parseInt(focused) === 1 + if (!outputClients[outputName]) { + outputClients[outputName] = 0 + } + const workspaceData = { id: tagId, idx: tagId, name: tagId.toString(), output: outputName, isActive: isActive, - isFocused: isFocused, + isFocused: isFocused && (outputName === selectedMonitor), isUrgent: isUrgent, isOccupied: isOccupied, clients: parseInt(clients) } - newWorkspaceCache[tagId] = workspaceData + newWorkspaceCache[`${outputName}-${tagId}`] = workspaceData workspacesList.push(workspaceData) } + const clientsMatch = trimmed.match(/^(\S+)\s+clients\s+(\d+)$/) + if (clientsMatch) { + const [, outputName, clientCount] = clientsMatch + outputClients[outputName] = parseInt(clientCount) + } + + const tagsMatch = trimmed.match(/^(\S+)\s+tags\s+(\d+)\s+(\d+)\s+(\d+)$/) + if (tagsMatch) { + const [, outputName, occ, seltags, urg] = tagsMatch + // Parse binary tag states for comprehensive workspace info + const occBits = occ.padStart(9, '0') + const selBits = seltags.padStart(9, '0') + const urgBits = urg.padStart(9, '0') + + for (let i = 0; i < 9; i++) { + const tagId = i + 1 + const isActive = selBits[8-i] === '1' + const isUrgent = urgBits[8-i] === '1' + const isOccupied = occBits[8-i] === '1' + + const workspaceData = { + id: tagId, + idx: tagId, + name: tagId.toString(), + output: outputName, + isActive: isActive, + isFocused: false, // Will be determined by selected monitor + isUrgent: isUrgent, + isOccupied: isOccupied, + clients: 0 // Will be updated by tag-specific data + } + + const key = `${outputName}-${tagId}` + if (!newWorkspaceCache[key]) { + newWorkspaceCache[key] = workspaceData + workspacesList.push(workspaceData) + } + } + } + const layoutMatch = trimmed.match(/^(\S+)\s+layout\s+(\S+)$/) if (layoutMatch) { const [, , layoutSymbol] = layoutMatch @@ -444,21 +542,21 @@ Item { } } - if (JSON.stringify(newWorkspaceCache) !== JSON.stringify(workspaceCache)) { - workspaceCache = newWorkspaceCache - - workspacesList.sort((a, b) => { - if (a.id !== b.id) return a.id - b.id - return a.output.localeCompare(b.output) - }) + if (JSON.stringify(newWorkspaceCache) !== JSON.stringify(workspaceCache)) { + workspaceCache = newWorkspaceCache + + workspacesList.sort((a, b) => { + if (a.id !== b.id) return a.id - b.id + return a.output.localeCompare(b.output) + }) - workspaces.clear() - for (var i = 0; i < workspacesList.length; i++) { - workspaces.append(workspacesList[i]) + workspaces.clear() + for (var i = 0; i < workspacesList.length; i++) { + workspaces.append(workspacesList[i]) + } + + workspaceChanged() } - - workspaceChanged() - } } function parseWindows(windowData) { @@ -468,17 +566,25 @@ Item { for (const [outputName, data] of Object.entries(windowData)) { if (data.title || data.appId) { - // Windows from mmsg -g -c are already the focused windows for their respective outputs + // Windows from mmsg -g -o -c are the focused windows for their respective outputs // A window is focused if it's from the currently selected monitor - // If selectedMonitor is not yet set, assume first window is focused (fallback) - const isFocused = selectedMonitor ? (outputName === selectedMonitor) : (windowsList.length === 0) + const isFocused = (outputName === selectedMonitor) + + // Get the active tag for this output + let activeTagId = defaultWorkspaceId + for (const [key, tagData] of Object.entries(workspaceCache)) { + if (tagData.output === outputName && tagData.isActive) { + activeTagId = tagData.id + break + } + } const windowInfo = { - id: outputName, + id: `${outputName}-${data.appId || 'unknown'}`, title: data.title || "", appId: data.appId || "", class: data.appId || "", - workspaceId: getCurrentActiveTagId(), + workspaceId: activeTagId, isFocused: isFocused, output: outputName, fullscreen: data.fullscreen || false, @@ -496,7 +602,7 @@ Item { } windowsList.push(windowInfo) - newWindowCache[outputName] = windowInfo + newWindowCache[windowInfo.id] = windowInfo if (isFocused) { newFocusedIndex = windowsList.length - 1 @@ -582,8 +688,11 @@ Item { case "layout": case "kb_layout": case "scale_factor": - case "monitor": - case "client": + case "toggle": + case "last_layer": + case "keymode": + case "clients": + case "tags": updateTimer.restart() break } @@ -610,6 +719,14 @@ Item { outputsProcess.running = true } + function queryDisplayScales() { + queryOutputs() + } + + function queryOutputEnum() { + outputEnumProcess.running = true + } + function queryMonitorState() { monitorStateProcess.running = true } @@ -626,11 +743,17 @@ Item { } function getCurrentActiveTagId() { - for (const [tagId, tagData] of Object.entries(workspaceCache)) { + for (const [key, tagData] of Object.entries(workspaceCache)) { + if (tagData.isActive && tagData.output === selectedMonitor) { + return tagData.id + } + } + // Fallback to any active tag if no selected monitor match + for (const [key, tagData] of Object.entries(workspaceCache)) { if (tagData.isActive) { - return parseInt(tagId) + return tagData.id } } return defaultWorkspaceId } -} \ No newline at end of file +} From 95d252a9496b3bcd2aab343dbd7400d5385879ea Mon Sep 17 00:00:00 2001 From: atheeq-rhxn Date: Fri, 14 Nov 2025 17:05:34 +0530 Subject: [PATCH 7/8] fix: single monitor support by conditional -o usage --- Services/Compositor/MangoService.qml | 288 +++++++++++++++------------ 1 file changed, 164 insertions(+), 124 deletions(-) diff --git a/Services/Compositor/MangoService.qml b/Services/Compositor/MangoService.qml index 64d0676c..23a9fc84 100644 --- a/Services/Compositor/MangoService.qml +++ b/Services/Compositor/MangoService.qml @@ -5,33 +5,36 @@ import qs.Commons import qs.Services.UI import qs.Services.Keyboard +// MangoService integrates with MangoWC compositor using mmsg IPC commands +// for real-time window management, workspace control, and state monitoring + Item { id: root - // Properties matching facade interface + // Facade interface properties property ListModel workspaces: ListModel {} property var windows: [] property int focusedWindowIndex: -1 - // Signals matching facade interface + // Facade interface signals signal workspaceChanged signal activeWindowChanged signal windowListChanged signal displayScalesChanged - // Mango-specific properties - property bool initialized: false - property bool overviewActive: false - property var workspaceCache: ({}) - property var windowCache: ({}) - property var monitorCache: ({}) - property string currentLayout: "" - property string currentLayoutSymbol: "" - property string currentKeyboardLayout: "" - property string selectedMonitor: "" + // MangoWC-specific state + property bool initialized: false + property bool overviewActive: false + property var workspaceCache: ({}) // Cache for workspace data to detect changes + property var windowCache: ({}) // Cache for window data to detect changes + property var monitorCache: ({}) // Cache for monitor/scale data + property string currentLayout: "" // Current layout name + property string currentLayoutSymbol: "" // Current layout symbol (e.g., 'S' for scroller) + property string currentKeyboardLayout: "" // Current keyboard layout name + property string selectedMonitor: "" // Currently selected/focused monitor - // Constants - readonly property var mmsgCommands: ({ + // mmsg command templates for MangoWC IPC (mmsg is the MangoWC message interface) + readonly property var mmsgCommands: ({ query: { workspaces: ["mmsg", "-g", "-t"], windows: ["mmsg", "-g", "-c"], @@ -52,18 +55,20 @@ Item { } }) - readonly property string overviewLayoutSymbol: "󰃇" - readonly property int defaultWorkspaceId: 1 + readonly property string overviewLayoutSymbol: "󰃇" // Symbol representing overview layout + readonly property int defaultWorkspaceId: 1 // Default workspace ID when none specified - // Debounce timer for updates - Timer { - id: updateTimer - interval: 50 - repeat: false - onTriggered: safeUpdate() - } + // Debounce timer for rapid state changes to avoid excessive updates + Timer { + id: updateTimer + interval: 50 + repeat: false + onTriggered: safeUpdate() + } - // Event stream for real-time updates + + // Event stream process for real-time MangoWC state monitoring using mmsg -w + // Monitors events: workspace changes, window focus/movement, layout changes, monitor selection Process { id: eventStream running: false @@ -87,22 +92,24 @@ Item { } } - Timer { - id: restartTimer - interval: 1000 - onTriggered: { - if (initialized) { - eventStream.running = true - } - } - } + // Restart timer for event stream recovery on failure + Timer { + id: restartTimer + interval: 1000 + onTriggered: { + if (initialized) { + eventStream.running = true + } + } + } - // Query processes - Process { - id: workspacesProcess - running: false - command: mmsgCommands.query.workspaces - property string accumulatedOutput: "" + + // Process to query workspaces using mmsg -g -t + Process { + id: workspacesProcess + running: false + command: mmsgCommands.query.workspaces + property string accumulatedOutput: "" stdout: SplitParser { onRead: function (line) { @@ -120,12 +127,13 @@ Item { } } - Process { - id: windowsProcess - running: false - command: mmsgCommands.query.windows - property string accumulatedOutput: "" - property var currentWindow: ({}) + // Process to query windows using mmsg -g -c + Process { + id: windowsProcess + running: false + command: mmsgCommands.query.windows + property string accumulatedOutput: "" + property var currentWindow: ({}) onRunningChanged: { if (running) { @@ -193,10 +201,11 @@ Item { } } - Process { - id: layoutProcess - running: false - command: mmsgCommands.query.layout + // Process to query current layout using mmsg -g -l + Process { + id: layoutProcess + running: false + command: mmsgCommands.query.layout stdout: SplitParser { onRead: function (line) { @@ -219,10 +228,11 @@ Item { } } - Process { - id: keyboardProcess - running: false - command: mmsgCommands.query.keyboard + // Process to query keyboard layout using mmsg -g -k + Process { + id: keyboardProcess + running: false + command: mmsgCommands.query.keyboard stdout: SplitParser { onRead: function (line) { @@ -248,10 +258,11 @@ Item { } } - Process { - id: outputsProcess - running: false - command: mmsgCommands.query.outputs + // Process to query output scales using mmsg -g -A + Process { + id: outputsProcess + running: false + command: mmsgCommands.query.outputs stdout: SplitParser { onRead: function (line) { @@ -283,10 +294,11 @@ Item { } } - Process { - id: monitorStateProcess - running: false - command: mmsgCommands.query.monitors + // Process to query monitor states using mmsg -g -o + Process { + id: monitorStateProcess + running: false + command: mmsgCommands.query.monitors stdout: SplitParser { onRead: function (line) { @@ -313,16 +325,17 @@ Item { } } - Process { - id: outputEnumProcess - running: false - command: ["mmsg", "-g", "-O"] + // Process to enumerate available outputs using mmsg -g -O + Process { + id: outputEnumProcess + running: false + command: ["mmsg", "-g", "-O"] stdout: SplitParser { onRead: function (line) { try { const trimmed = line.trim() - // Handle output enumeration format: "+ eDP-1" + const outputName = trimmed.replace(/^\+\s*/, '') if (outputName && !monitorCache[outputName]) { monitorCache[outputName] = { @@ -345,7 +358,8 @@ Item { } } - // Initialization + + // Initialize MangoService and establish connection to MangoWC function initialize() { if (initialized) { Logger.w("MangoService", "Already initialized") @@ -355,7 +369,6 @@ Item { try { Logger.i("MangoService", "Initializing MangoWC service...") - // Query monitor state first to establish selected monitor before parsing windows queryOutputEnum() queryMonitorState() eventStream.running = true @@ -373,36 +386,47 @@ Item { } } - // Workspace operations + + + + // Switch to a specific workspace/tag function switchToWorkspace(workspace) { try { const tagId = workspace.idx || workspace.id || defaultWorkspaceId const outputName = workspace.output || selectedMonitor || "" let command = [...mmsgCommands.action.tag] - if (outputName) { + // Only add -o parameter for multi-monitor setups + if (outputName && Object.keys(monitorCache).length > 1) { command.push("-o", outputName) } command.push(tagId.toString()) Quickshell.execDetached(command) - Logger.d("MangoService", `Switching to workspace ${tagId} on ${outputName || 'default output'}`) } catch (e) { Logger.e("MangoService", "Failed to switch workspace:", e) } } - // Window operations + + // Focus a specific window on its workspace function focusWindow(window) { try { if (window && window.output) { let command = [...mmsgCommands.action.view] - command.push("-o", window.output, window.workspaceId.toString()) + const isMultiMonitor = Object.keys(monitorCache).length > 1 + + if (isMultiMonitor) { + command.push("-o", window.output) + } + command.push(window.workspaceId.toString()) Quickshell.execDetached(command) Qt.callLater(() => { let focusCommand = [...mmsgCommands.action.focusMaster] - focusCommand.push("-o", window.output) + if (isMultiMonitor) { + focusCommand.push("-o", window.output) + } Quickshell.execDetached(focusCommand) }) } @@ -414,7 +438,7 @@ Item { function closeWindow(window) { try { const command = [...mmsgCommands.action.killClient] - if (selectedMonitor) { + if (selectedMonitor && Object.keys(monitorCache).length > 1) { command.push("-o", selectedMonitor) } Quickshell.execDetached(command) @@ -423,11 +447,11 @@ Item { } } - // MangoWC-specific operations + function toggleOverview() { try { const command = [...mmsgCommands.action.toggleOverview] - if (selectedMonitor) { + if (selectedMonitor && Object.keys(monitorCache).length > 1) { command.push("-o", selectedMonitor) } Quickshell.execDetached(command) @@ -454,7 +478,10 @@ Item { } } - // Data parsing + + // Parse workspace data from mmsg -g -t output + // Handles formats: tag details, tag masks, and binary states + // State bits: bit 0 = active/selected, bit 1 = urgent function parseWorkspaces(output) { const lines = output.trim().split('\n') const workspacesList = [] @@ -504,7 +531,7 @@ Item { const tagsMatch = trimmed.match(/^(\S+)\s+tags\s+(\d+)\s+(\d+)\s+(\d+)$/) if (tagsMatch) { const [, outputName, occ, seltags, urg] = tagsMatch - // Parse binary tag states for comprehensive workspace info + const occBits = occ.padStart(9, '0') const selBits = seltags.padStart(9, '0') const urgBits = urg.padStart(9, '0') @@ -559,18 +586,18 @@ Item { } } - function parseWindows(windowData) { + // Parse window data from mmsg -g -c output into window list + function parseWindows(windowData) { const windowsList = [] const newWindowCache = {} let newFocusedIndex = -1 for (const [outputName, data] of Object.entries(windowData)) { if (data.title || data.appId) { - // Windows from mmsg -g -o -c are the focused windows for their respective outputs - // A window is focused if it's from the currently selected monitor + const isFocused = (outputName === selectedMonitor) - // Get the active tag for this output + let activeTagId = defaultWorkspaceId for (const [key, tagData] of Object.entries(workspaceCache)) { if (tagData.output === outputName && tagData.isActive) { @@ -624,7 +651,8 @@ Item { } } - function handleLayoutChange(layoutSymbol) { + // Handle layout change events and update overview state + function handleLayoutChange(layoutSymbol) { const wasOverview = overviewActive const isOverview = (layoutSymbol === overviewLayoutSymbol) @@ -639,7 +667,8 @@ Item { } } - function updateDisplayScales() { + // Update display scales and notify CompositorService + function updateDisplayScales() { const scales = {} for (const [outputName, data] of Object.entries(monitorCache)) { scales[outputName] = { @@ -661,8 +690,9 @@ Item { displayScalesChanged() } - // Event handling - function handleEvent(eventLine) { + + // Handle real-time events from mmsg -w event stream and trigger updates + function handleEvent(eventLine) { const parts = eventLine.trim().split(/\s+/) if (parts.length < 2) return @@ -698,57 +728,67 @@ Item { } } - // Queries - function queryWorkspaces() { - workspacesProcess.running = true - } - function queryWindows() { - windowsProcess.running = true - } + // Start workspace query process + function queryWorkspaces() { + workspacesProcess.running = true + } - function queryLayout() { - layoutProcess.running = true - } + // Start window query process + function queryWindows() { + windowsProcess.running = true + } - function queryKeyboard() { - keyboardProcess.running = true - } + // Start layout query process + function queryLayout() { + layoutProcess.running = true + } - function queryOutputs() { - outputsProcess.running = true - } + // Start keyboard layout query process + function queryKeyboard() { + keyboardProcess.running = true + } - function queryDisplayScales() { - queryOutputs() - } + // Start output scales query process + function queryOutputs() { + outputsProcess.running = true + } - function queryOutputEnum() { - outputEnumProcess.running = true - } + // Query display scales (alias for queryOutputs) + function queryDisplayScales() { + queryOutputs() + } - function queryMonitorState() { - monitorStateProcess.running = true - } + // Start output enumeration process + function queryOutputEnum() { + outputEnumProcess.running = true + } - // Utilities - function safeUpdate() { - try { - queryWorkspaces() - queryWindows() - queryMonitorState() - } catch (e) { - Logger.e("MangoService", "Safe update failed:", e) - } - } + // Start monitor state query process + function queryMonitorState() { + monitorStateProcess.running = true + } - function getCurrentActiveTagId() { + + // Safely update all state by querying workspaces, windows, and monitor state + function safeUpdate() { + try { + queryWorkspaces() + queryWindows() + queryMonitorState() + } catch (e) { + Logger.e("MangoService", "Safe update failed:", e) + } + } + + // Get the ID of the currently active workspace/tag + function getCurrentActiveTagId() { for (const [key, tagData] of Object.entries(workspaceCache)) { if (tagData.isActive && tagData.output === selectedMonitor) { return tagData.id } } - // Fallback to any active tag if no selected monitor match + for (const [key, tagData] of Object.entries(workspaceCache)) { if (tagData.isActive) { return tagData.id From 73269047ca580b638edccad7c7ec5d8d43db4558 Mon Sep 17 00:00:00 2001 From: atheeq-rhxn Date: Fri, 14 Nov 2025 20:48:46 +0530 Subject: [PATCH 8/8] fix: show empty workspaces when hideUnoccupied enabled --- Services/Compositor/MangoService.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Services/Compositor/MangoService.qml b/Services/Compositor/MangoService.qml index 23a9fc84..2ad63e0f 100644 --- a/Services/Compositor/MangoService.qml +++ b/Services/Compositor/MangoService.qml @@ -512,7 +512,7 @@ Item { name: tagId.toString(), output: outputName, isActive: isActive, - isFocused: isFocused && (outputName === selectedMonitor), + isFocused: isFocused || (isActive && (outputName === selectedMonitor)), isUrgent: isUrgent, isOccupied: isOccupied, clients: parseInt(clients)