mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
810 lines
24 KiB
QML
810 lines
24 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
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
|
|
|
|
// Facade interface properties
|
|
property ListModel workspaces: ListModel {}
|
|
property var windows: []
|
|
property int focusedWindowIndex: -1
|
|
|
|
// Facade interface signals
|
|
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
|
|
|
|
// 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"]
|
|
}
|
|
})
|
|
|
|
readonly property string overviewLayoutSymbol: "" // Symbol representing overview layout
|
|
readonly property int defaultWorkspaceId: 1 // Default workspace ID when none specified
|
|
|
|
// Debounce timer for rapid state changes to avoid excessive updates
|
|
Timer {
|
|
id: updateTimer
|
|
interval: 50
|
|
repeat: false
|
|
onTriggered: safeUpdate()
|
|
}
|
|
|
|
// 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
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Restart timer for event stream recovery on failure
|
|
Timer {
|
|
id: restartTimer
|
|
interval: 1000
|
|
onTriggered: {
|
|
if (initialized) {
|
|
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";
|
|
}
|
|
}
|
|
|
|
onExited: function (exitCode) {
|
|
if (exitCode === 0) {
|
|
parseWorkspaces(accumulatedOutput);
|
|
} else {
|
|
Logger.e("MangoService", "Workspaces query failed:", exitCode);
|
|
}
|
|
accumulatedOutput = "";
|
|
}
|
|
}
|
|
|
|
// 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 = {};
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|