diff --git a/Services/Compositor/MangoService.qml b/Services/Compositor/MangoService.qml index cb8aef42..07b8b34d 100644 --- a/Services/Compositor/MangoService.qml +++ b/Services/Compositor/MangoService.qml @@ -1,809 +1,463 @@ import QtQuick import Quickshell import Quickshell.Io +import Quickshell.Wayland import qs.Commons import qs.Services.Keyboard import qs.Services.UI -// MangoService integrates with MangoWC compositor using mmsg IPC commands -// for real-time window management, workspace control, and state monitoring Item { - id: root + id: root - // Facade interface properties - property ListModel workspaces: ListModel {} - property var windows: [] - property int focusedWindowIndex: -1 + // 1. FACADE INTERFACE + property ListModel workspaces: ListModel {} + property var windows: [] + property int focusedWindowIndex: -1 - // Facade interface signals - signal workspaceChanged - signal activeWindowChanged - signal windowListChanged - signal displayScalesChanged + signal workspaceChanged + signal activeWindowChanged + signal windowListChanged + signal displayScalesChanged - // 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 + property string selectedMonitor: "" + property string currentLayoutSymbol: "" + property bool initialized: false - // 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"], - "layout": ["mmsg", "-g", "-l"], - "keyboard": ["mmsg", "-g", "-k"], - "outputs": ["mmsg", "-g", "-A"], - "monitors": ["mmsg", "-g", "-o"], - "eventStream": ["mmsg", "-w"] - }, - "action": { - "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"] - } - }) + // 2. CONFIGURATION + QtObject { + id: config + readonly property int defaultWorkspaceId: 1 + + // Pre-compiled Regex for Performance + readonly property var reTagDetail: /^(\S+)\s+tag\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$/ + readonly property var reTagBinary: /^(\S+)\s+tags\s+([01]+)\s+([01]+)\s+([01]+)$/ + readonly property var reLayout: /^(\S+)\s+layout\s+(\S+)$/ + readonly property var reMetadata: /^(\S+)\s+(title|appid)\s+(.*)$/ + readonly property var reKbLayout: /^(\S+)\s+kb_layout\s+(.*)$/ + readonly property var reScale: /^(\S+)\s+scale_factor\s+(\d+(\.\d+)?)$/ + + readonly property var query: ({ + eventStream: ["mmsg", "-w"], + monitors: ["mmsg", "-g", "-o"], + outputs: ["mmsg", "-g", "-A"], // Scales + workspaces: ["mmsg", "-g", "-t"] + }) - readonly property string overviewLayoutSymbol: "󰃇" // Symbol representing overview layout - readonly property int defaultWorkspaceId: 1 // Default workspace ID when none specified + readonly property var action: ({ + tag: ["mmsg", "-s", "-t"], + view: ["mmsg", "-s", "-d", "view"], + toggleOverview: ["mmsg", "-s", "-d", "toggleoverview"], + setLayout: ["mmsg", "-s", "-d", "setlayout"], + killClient: ["mmsg", "-s", "-d", "killclient"], + quit: ["mmsg", "-s", "-q"] + }) + } - // Debounce timer for rapid state changes to avoid excessive updates - Timer { - id: updateTimer - interval: 50 - repeat: false - onTriggered: safeUpdate() - } + // 3. LOGIC ENGINE + QtObject { + id: internal - // 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 - command: mmsgCommands.query.eventStream + // State + property var activeTags: ({}) + property var multiTagState: ({}) + property bool hasValidTagData: false + + // Map + property var windowStateMap: new Map() + + property string mmsgFocusedTitle: "" + property string mmsgFocusedAppId: "" + property string currentKbLayout: "" + + // Caches + property var workspaceCache: ({}) + property var monitorScales: ({}) + property string lastWindowSig: "" - stdout: SplitParser { - onRead: function (line) { - try { - handleEvent(line.trim()); - } catch (e) { - Logger.e("MangoService", "Event parsing error:", e, line); + // Buffers + property string streamBuffer: "" + + // --- STREAM PROCESSOR --- + function processFrame(output) { + const lines = output.trim().split('\n'); + const newWsList = []; + const newWsCache = {}; + const processedTags = {}; + let metadataChanged = false; + let receivedClientCounts = false; // Track if we got real numbers + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + if (!line) continue; + + // 1. Tag Details (High Quality Data: Contains Client Counts) + const tagMatch = line.match(config.reTagDetail); + if (tagMatch) { + const outputName = tagMatch[1]; + const tagId = parseInt(tagMatch[2]); + const state = parseInt(tagMatch[3]); + const clients = parseInt(tagMatch[4]); + const focused = parseInt(tagMatch[5]); + const isActive = (state & 1) !== 0; + + if (isActive) internal.activeTags[outputName] = tagId; + const wsData = { + "id": tagId, "idx": tagId, "name": tagId.toString(), + "output": outputName, + "isActive": isActive, + "isFocused": isActive && (focused === 1 || outputName === root.selectedMonitor), + "isUrgent": (state & 2) !== 0, + "isOccupied": clients > 0, + "clients": clients // We have the real number! + }; + const key = `${outputName}-${tagId}`; + newWsCache[key] = wsData; + newWsList.push(wsData); + processedTags[key] = true; + receivedClientCounts = true; + continue; + } + + // 2. Binary Tags (Low Quality Data: No Client Counts) + const tagsMatch = line.match(config.reTagBinary); + if (tagsMatch) { + const outputName = tagsMatch[1]; + const occ = tagsMatch[2]; + const seltags = tagsMatch[3]; + const urg = tagsMatch[4]; + const len = occ.length; + + // Overview Detection + let activeCount = 0; + for (let c = 0; c < seltags.length; c++) if (seltags[c] === '1') activeCount++; + internal.multiTagState[outputName] = (activeCount > 1); + + for (let j = 0; j < len; j++) { + const tagId = j + 1; + const charIdx = len - 1 - j; + const key = `${outputName}-${tagId}`; + + if (processedTags[key]) continue; + const isActive = seltags[charIdx] === '1'; + const isOccupied = occ[charIdx] === '1'; + + if (isActive) internal.activeTags[outputName] = tagId; + newWsList.push({ + "id": tagId, "idx": tagId, "name": tagId.toString(), + "output": outputName, + "isActive": isActive, + "isFocused": false, + "isUrgent": urg[charIdx] === '1', + "isOccupied": isOccupied, + "clients": isOccupied ? -1 : 0 // -1 indicates "Unknown, but at least 1" + }); + } + continue; + } + + // 3. Metadata + const metaMatch = line.match(config.reMetadata); + if (metaMatch) { + const prop = metaMatch[2]; + const val = metaMatch[3]; + if (prop === "title") internal.mmsgFocusedTitle = val; + else if (prop === "appid") internal.mmsgFocusedAppId = val; + metadataChanged = true; + continue; + } + + // 4. Layout + const layoutMatch = line.match(config.reLayout); + if (layoutMatch) { + if (layoutMatch[2] !== root.currentLayoutSymbol) root.currentLayoutSymbol = layoutMatch[2]; + } + + // 5. Keyboard + const kbMatch = line.match(config.reKbLayout); + if (kbMatch) { + const kbName = kbMatch[2]; + if (kbName !== internal.currentKbLayout) { + internal.currentKbLayout = kbName; + if (KeyboardLayoutService) KeyboardLayoutService.setCurrentLayout(kbName); + } + } + } + + // Apply + if (JSON.stringify(newWsCache) !== JSON.stringify(internal.workspaceCache)) { + internal.workspaceCache = newWsCache; + newWsList.sort((a, b) => { + if (a.id !== b.id) return a.id - b.id; + return a.output.localeCompare(b.output); + }); + root.workspaces.clear(); + for (let k = 0; k < newWsList.length; k++) root.workspaces.append(newWsList[k]); + root.workspaceChanged(); + + if (receivedClientCounts) internal.hasValidTagData = true; + } + + if (metadataChanged || newWsList.length > 0) { + internal.updateWindowList(); + } + } + + // --- WINDOW LIST MERGE --- + function updateWindowList() { + if (!ToplevelManager.toplevels) return; + + // FIX: SAFETY CHECK + // If we haven't received "Detailed" data yet (only binary), + // we don't know where the windows are. Abort to prevent map poisoning. + if (!internal.hasValidTagData) return; + + const rawList = ToplevelManager.toplevels.values; + const finalList = []; + let newFocusedIdx = -1; + + const currentObjects = new Set(); + const tagCapacities = new Map(); + + // Build Capacities + let totalCapacity = 0; + for (let key in internal.workspaceCache) { + const ws = internal.workspaceCache[key]; + const cap = ws.clients > 0 ? ws.clients : 0; + tagCapacities.set(key, cap); + totalCapacity += cap; + } + + + if (totalCapacity === 0 && rawList.length > 0 && !internal.hasValidTagData) return; + + const unassignedWindows = []; + + // Pass 1: Known & Focused + for (let i = 0; i < rawList.length; i++) { + const toplevel = rawList[i]; + if (!toplevel || toplevel.outliers) continue; + + currentObjects.add(toplevel); + + const appId = toplevel.appId || toplevel.wayland.appId || ""; + const title = toplevel.title || toplevel.wayland.title || ""; + + let outputName = root.selectedMonitor; + if (toplevel.outputs && toplevel.outputs.length > 0) { + outputName = toplevel.outputs[0].name; + } + + const currentActiveTag = internal.activeTags[outputName] || config.defaultWorkspaceId; + const isMultiTag = internal.multiTagState[outputName] === true; + let wsId = -1; + + const isFocused = toplevel.activated; + const isMmsgFocus = (title === internal.mmsgFocusedTitle) && + (appId === internal.mmsgFocusedAppId); + + if (isFocused && isMmsgFocus && !isMultiTag) { + wsId = currentActiveTag; + internal.windowStateMap.set(toplevel, wsId); + } + else if (internal.windowStateMap.has(toplevel)) { + wsId = internal.windowStateMap.get(toplevel); + } + + if (wsId !== -1) { + const key = `${outputName}-${wsId}`; + const cap = tagCapacities.get(key) || 0; + if (cap > 0) tagCapacities.set(key, cap - 1); + + finalList.push(createWindowObject(toplevel, outputName, appId, title, wsId, isFocused, i)); + } else { + unassignedWindows.push({ toplevel, outputName, appId, title, isFocused, index: i }); + } + } + + // Pass 2: Distribute Unknowns + for (const win of unassignedWindows) { + let assignedId = -1; + + for (let key in internal.workspaceCache) { + const ws = internal.workspaceCache[key]; + // Robust output check: match name OR if both are undefined/generic + if (ws.output !== win.outputName && win.outputName !== "") continue; + + const cap = tagCapacities.get(key) || 0; + if (cap > 0) { + assignedId = ws.id; + tagCapacities.set(key, cap - 1); + break; + } + } + + if (assignedId === -1) { + assignedId = internal.activeTags[win.outputName] || config.defaultWorkspaceId; + } + + internal.windowStateMap.set(win.toplevel, assignedId); + finalList.push(createWindowObject( + win.toplevel, win.outputName, win.appId, win.title, assignedId, win.isFocused, win.index + )); + } + + finalList.sort((a, b) => { + const idxA = parseInt(a.id.split('-').pop()); + const idxB = parseInt(b.id.split('-').pop()); + return idxA - idxB; + }); + + for(let i=0; i rawList.length + 10) { + for (const key of internal.windowStateMap.keys()) { + if (!currentObjects.has(key)) internal.windowStateMap.delete(key); + } + } + + const sig = JSON.stringify(finalList.map(w => w.id + w.workspaceId + w.isFocused)); + if (sig !== internal.lastWindowSig) { + internal.lastWindowSig = sig; + root.windows = finalList; + root.windowListChanged(); + } + + if (newFocusedIdx !== root.focusedWindowIndex) { + root.focusedWindowIndex = newFocusedIdx; + root.activeWindowChanged(); + } + } + + function createWindowObject(toplevel, outputName, appId, title, wsId, isFocused, index) { + return { + "id": `${outputName}-${appId}-${index}`, + "title": title, + "appId": appId, + "class": appId, + "workspaceId": wsId, + "isFocused": isFocused, + "output": outputName, + "handle": toplevel, + "fullscreen": toplevel.fullscreen, + "floating": toplevel.maximized === false && toplevel.fullscreen === false + }; + } + + // --- SCALES --- + function updateScales() { + const scalesMap = {}; + for (const [name, data] of Object.entries(internal.monitorScales)) { + scalesMap[name] = { + "name": name, + "scale": data.scale || 1.0, + "width": 0, "height": 0, "x": 0, "y": 0 + }; + } + if (CompositorService && CompositorService.onDisplayScalesUpdated) { + CompositorService.onDisplayScalesUpdated(scalesMap); + } + root.displayScalesChanged(); } - } } - onExited: function (exitCode) { - if (exitCode !== 0) { - Logger.e("MangoService", "Event stream exited, restarting..."); - restartTimer.start(); - } - } - } + // 4. PROCESSES - // Restart timer for event stream recovery on failure - Timer { - id: restartTimer - interval: 1000 - onTriggered: { - if (initialized) { + Process { + id: eventStream + running: false + command: config.query.eventStream + stdout: SplitParser { + onRead: (line) => { + if (line.includes("selmon") && line.includes(" 1")) { + const parts = line.split(' '); + if (parts.length >= 3) root.selectedMonitor = parts[0]; + return; + } + internal.streamBuffer += line + "\n"; + if (line.match(config.reTagBinary)) { + internal.processFrame(internal.streamBuffer); + internal.streamBuffer = ""; + } + } + } + onExited: (code) => { if (code !== 0) restartTimer.start(); } + } + Timer { id: restartTimer; interval: 1000; onTriggered: if(initialized) eventStream.running = true } + + Process { + id: procInitial + command: config.query.workspaces + stdout: SplitParser { onRead: (line) => internal.streamBuffer += line + "\n" } + onExited: (code) => { + if (code === 0) { + internal.processFrame(internal.streamBuffer); + internal.streamBuffer = ""; + } + } + } + + Process { + id: procOutputs + command: config.query.outputs + stdout: SplitParser { + onRead: (line) => { + const match = line.match(config.reScale); + if (match) { + const out = match[1]; + const scale = parseFloat(match[2]); + if (!internal.monitorScales[out]) internal.monitorScales[out] = {}; + internal.monitorScales[out].scale = scale; + } + } + } + onExited: (code) => { if (code === 0) internal.updateScales(); } + } + + // 5. WAYLAND & INIT + + Connections { + target: ToplevelManager + function onToplevelsChanged() { internal.updateWindowList(); } + } + Timer { + interval: 200; running: true; repeat: true + onTriggered: internal.updateWindowList() + } + + function initialize() { + if (initialized) return; + Logger.i("MangoService", "Service Started"); + + procOutputs.running = true; + procInitial.running = true; eventStream.running = true; - } - } - } - - // 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) { - workspacesProcess.accumulatedOutput += line + "\n"; - } + Quickshell.execDetached(["mmsg", "-g", "-o"]); + + initialized = true; } - onExited: function (exitCode) { - if (exitCode === 0) { - parseWorkspaces(accumulatedOutput); - } else { - Logger.e("MangoService", "Workspaces query failed:", exitCode); - } - accumulatedOutput = ""; - } - } + function queryDisplayScales() { procOutputs.running = true; } - // 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) { - windowsProcess.currentWindow = {}; - } + function switchToWorkspace(workspace) { + const tagId = workspace.idx || workspace.id || config.defaultWorkspaceId; + const output = workspace.output || root.selectedMonitor || ""; + const cmd = config.action.tag.slice(); + if (output && Object.keys(internal.monitorScales).length > 1) cmd.push("-o", output); + cmd.push(tagId.toString()); + Quickshell.execDetached(cmd); } - 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 (!windowsProcess.currentWindow[outputName]) { - windowsProcess.currentWindow[outputName] = { - "id": outputName, - "output": outputName - }; - } - - switch (property) { - case "title": - windowsProcess.currentWindow[outputName].title = value; - break; - case "appid": - windowsProcess.currentWindow[outputName].appId = value; - windowsProcess.currentWindow[outputName].class = value; - break; - case "fullscreen": - windowsProcess.currentWindow[outputName].fullscreen = (value === "1"); - break; - case "floating": - windowsProcess.currentWindow[outputName].floating = (value === "1"); - break; - case "x": - windowsProcess.currentWindow[outputName].x = parseInt(value); - break; - case "y": - windowsProcess.currentWindow[outputName].y = parseInt(value); - break; - case "width": - windowsProcess.currentWindow[outputName].width = parseInt(value); - break; - case "height": - windowsProcess.currentWindow[outputName].height = parseInt(value); - break; - } - } - } + function focusWindow(window) { + if (window && window.handle) window.handle.activate(); + else if (window.workspaceId) switchToWorkspace({ id: window.workspaceId, output: window.output }); } - onExited: function (exitCode) { - if (exitCode === 0) { - parseWindows(windowsProcess.currentWindow); - } else { - Logger.e("MangoService", "Windows query failed:", exitCode); - } - accumulatedOutput = ""; - windowsProcess.currentWindow = {}; - } - } - - // Process to query current layout using mmsg -g -l - 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); - } - } + function closeWindow(window) { + if (window && window.handle) window.handle.close(); + else Quickshell.execDetached(config.action.killClient); } - onExited: function (exitCode) { - if (exitCode !== 0) { - Logger.e("MangoService", "Layout query failed:", exitCode); - } - } - } - - // Process to query keyboard layout using mmsg -g -k - 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 to query output scales using mmsg -g -A - Process { - id: outputsProcess - running: false - command: mmsgCommands.query.outputs - - 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", "Output parsing error:", e, line); - } - } - } - - onExited: function (exitCode) { - if (exitCode === 0) { - updateDisplayScales(); - } else { - Logger.e("MangoService", "Outputs query failed:", exitCode); - } - } - } - - // Process to query monitor states using mmsg -g -o - Process { - id: monitorStateProcess - running: false - command: mmsgCommands.query.monitors - - 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); - } - } - } - - // 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(); - - 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); - } - } - } - - // Initialize MangoService and establish connection to MangoWC - function initialize() { - if (initialized) { - Logger.w("MangoService", "Already initialized"); - return; - } - - try { - Logger.i("MangoService", "Service started"); - - queryOutputEnum(); - queryMonitorState(); - 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; - } - } - - // 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.slice(); - - // 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); - } catch (e) { - Logger.e("MangoService", "Failed to switch workspace:", e); - } - } - - // Focus a specific window on its workspace - function focusWindow(window) { - try { - if (window && window.output) { - let command = mmsgCommands.action.view.slice(); - 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.slice(); - if (isMultiMonitor) { - focusCommand.push("-o", window.output); - } - Quickshell.execDetached(focusCommand); - }); - } - } catch (e) { - Logger.e("MangoService", "Failed to focus window:", e); - } - } - - function closeWindow(window) { - try { - const command = mmsgCommands.action.killClient.slice(); - if (selectedMonitor && Object.keys(monitorCache).length > 1) { - command.push("-o", selectedMonitor); - } - Quickshell.execDetached(command); - } catch (e) { - Logger.e("MangoService", "Failed to close window:", e); - } - } - - function toggleOverview() { - try { - const command = mmsgCommands.action.toggleOverview.slice(); - if (selectedMonitor && Object.keys(monitorCache).length > 1) { - command.push("-o", selectedMonitor); - } - Quickshell.execDetached(command); - } catch (e) { - Logger.e("MangoService", "Failed to toggle overview:", e); - } - } - - function setLayout(layoutName) { - try { - const command = mmsgCommands.action.setLayout.slice(); - command.push(layoutName); - Quickshell.execDetached(command); - } catch (e) { - Logger.e("MangoService", "Failed to set layout:", e); - } - } - - function logout() { - try { - Quickshell.execDetached(mmsgCommands.action.quit); - } catch (e) { - Logger.e("MangoService", "Failed to logout:", e); - } - } - - // 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 = []; - const newWorkspaceCache = {}; - let outputClients = {}; - - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed) - continue; - const tagMatch = trimmed.match(/^(\S+)\s+tag\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$/); - if (tagMatch) { - const outputName = tagMatch[1]; - const tagNum = tagMatch[2]; - const state = tagMatch[3]; - const clients = tagMatch[4]; - const focused = tagMatch[5]; - const tagId = parseInt(tagNum); - - const isActive = (parseInt(state) & 1) !== 0; - const isUrgent = (parseInt(state) & 2) !== 0; - 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 || (isActive && (outputName === selectedMonitor)), - "isUrgent": isUrgent, - "isOccupied": isOccupied, - "clients": parseInt(clients) - }; - - newWorkspaceCache[`${outputName}-${tagId}`] = workspaceData; - workspacesList.push(workspaceData); - } - - const clientsMatch = trimmed.match(/^(\S+)\s+clients\s+(\d+)$/); - if (clientsMatch) { - const outputName = clientsMatch[1]; - const clientCount = clientsMatch[2]; - outputClients[outputName] = parseInt(clientCount); - } - - const tagsMatch = trimmed.match(/^(\S+)\s+tags\s+(\d+)\s+(\d+)\s+(\d+)$/); - if (tagsMatch) { - const outputName = tagsMatch[1]; - const occ = tagsMatch[2]; - const seltags = tagsMatch[3]; - const urg = tagsMatch[4]; - - const occBits = occ.padStart(9, '0'); - const selBits = seltags.padStart(9, '0'); - const urgBits = urg.padStart(9, '0'); - - for (var 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, - "isUrgent"// Will be determined by selected monitor - : 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[2]; - handleLayoutChange(layoutSymbol); - } - } - - 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(); - } - } - - // Parse window data from mmsg -g -c output into window list - function parseWindows(windowData) { - const windowsList = []; - const newWindowCache = {}; - let newFocusedIndex = -1; - - const windowEntries = Object.entries(windowData); - for (var i = 0; i < windowEntries.length; i++) { - const outputName = windowEntries[i][0]; - const data = windowEntries[i][1]; - if (data.title || data.appId) { - const isFocused = (outputName === selectedMonitor); - - let activeTagId = defaultWorkspaceId; - const workspaceEntries = Object.entries(workspaceCache); - for (var j = 0; j < workspaceEntries.length; j++) { - const key = workspaceEntries[j][0]; - const tagData = workspaceEntries[j][1]; - if (tagData.output === outputName && tagData.isActive) { - activeTagId = tagData.id; - break; - } - } - - const windowInfo = { - "id": `${outputName}-${data.appId || 'unknown'}`, - "title": data.title || "", - "appId": data.appId || "", - "class": data.appId || "", - "workspaceId": activeTagId, - "isFocused": isFocused, - "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); - newWindowCache[windowInfo.id] = windowInfo; - - if (isFocused) { - newFocusedIndex = windowsList.length - 1; - Logger.d("MangoService", `Focused window detected: ${data.title} on ${outputName}`); - } - } - } - - if (JSON.stringify(newWindowCache) !== JSON.stringify(windowCache)) { - windowCache = newWindowCache; - windows = windowsList; - - if (newFocusedIndex !== focusedWindowIndex) { - focusedWindowIndex = newFocusedIndex; - activeWindowChanged(); - } - - windowListChanged(); - } - } - - // Handle layout change events and update overview state - 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; - } - } - - // Update display scales and notify CompositorService - function updateDisplayScales() { - const scales = {}; - const monitorEntries = Object.entries(monitorCache); - for (var i = 0; i < monitorEntries.length; i++) { - const outputName = monitorEntries[i][0]; - const data = monitorEntries[i][1]; - 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 - }; - } - - if (CompositorService && CompositorService.onDisplayScalesUpdated) { - CompositorService.onDisplayScalesUpdated(scales); - } - displayScalesChanged(); - } - - // 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; - 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": - case "fullscreen": - case "floating": - case "layout": - case "kb_layout": - case "scale_factor": - case "toggle": - case "last_layer": - case "keymode": - case "clients": - case "tags": - updateTimer.restart(); - break; - } - } - - // Start workspace query process - function queryWorkspaces() { - workspacesProcess.running = true; - } - - // Start window query process - function queryWindows() { - windowsProcess.running = true; - } - - // Start layout query process - function queryLayout() { - layoutProcess.running = true; - } - - // Start keyboard layout query process - function queryKeyboard() { - keyboardProcess.running = true; - } - - // Start output scales query process - function queryOutputs() { - outputsProcess.running = true; - } - - // Query display scales (alias for queryOutputs) - function queryDisplayScales() { - queryOutputs(); - } - - // Start output enumeration process - function queryOutputEnum() { - outputEnumProcess.running = true; - } - - // Start monitor state query process - function queryMonitorState() { - monitorStateProcess.running = true; - } - - // 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() { - const workspaceEntries1 = Object.entries(workspaceCache); - for (var i = 0; i < workspaceEntries1.length; i++) { - const key = workspaceEntries1[i][0]; - const tagData = workspaceEntries1[i][1]; - if (tagData.isActive && tagData.output === selectedMonitor) { - return tagData.id; - } - } - - const workspaceEntries2 = Object.entries(workspaceCache); - for (var i = 0; i < workspaceEntries2.length; i++) { - const key = workspaceEntries2[i][0]; - const tagData = workspaceEntries2[i][1]; - if (tagData.isActive) { - return tagData.id; - } - } - return defaultWorkspaceId; - } + function logout() { Quickshell.execDetached(config.action.quit); } }