mirror of
https://github.com/zoriya/noctalia-shell.git
synced 2026-06-06 03:55:02 +00:00
Mango: fixed connection "on toplevels changed".
This commit is contained in:
@@ -7,457 +7,520 @@ import qs.Services.Keyboard
|
||||
import qs.Services.UI
|
||||
|
||||
Item {
|
||||
id: root
|
||||
id: root
|
||||
|
||||
// 1. FACADE INTERFACE
|
||||
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
|
||||
|
||||
signal workspaceChanged
|
||||
signal activeWindowChanged
|
||||
signal windowListChanged
|
||||
signal displayScalesChanged
|
||||
signal workspaceChanged
|
||||
signal activeWindowChanged
|
||||
signal windowListChanged
|
||||
signal displayScalesChanged
|
||||
|
||||
property string selectedMonitor: ""
|
||||
property string currentLayoutSymbol: ""
|
||||
property bool initialized: false
|
||||
property string selectedMonitor: ""
|
||||
property string currentLayoutSymbol: ""
|
||||
property bool initialized: false
|
||||
|
||||
// 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"]
|
||||
})
|
||||
// 2. CONFIGURATION
|
||||
QtObject {
|
||||
id: config
|
||||
readonly property int defaultWorkspaceId: 1
|
||||
|
||||
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"]
|
||||
})
|
||||
// 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 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"]
|
||||
})
|
||||
}
|
||||
|
||||
// 3. LOGIC ENGINE
|
||||
QtObject {
|
||||
id: internal
|
||||
|
||||
// State
|
||||
property var activeTags: ({})
|
||||
property var multiTagState: ({})
|
||||
property bool hasValidTagData: false
|
||||
|
||||
// Map<ToplevelObject, WorkspaceID>
|
||||
property var windowStateMap: new Map()
|
||||
|
||||
property string mmsgFocusedTitle: ""
|
||||
property string mmsgFocusedAppId: ""
|
||||
property string currentKbLayout: ""
|
||||
|
||||
// Caches
|
||||
property var workspaceCache: ({})
|
||||
property var monitorScales: ({})
|
||||
property string lastWindowSig: ""
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
// 3. LOGIC ENGINE
|
||||
QtObject {
|
||||
id: internal
|
||||
// --- WINDOW LIST MERGE ---
|
||||
function updateWindowList() {
|
||||
if (!ToplevelManager.toplevels)
|
||||
return;
|
||||
|
||||
// State
|
||||
property var activeTags: ({})
|
||||
property var multiTagState: ({})
|
||||
property bool hasValidTagData: false
|
||||
|
||||
// Map<ToplevelObject, WorkspaceID>
|
||||
property var windowStateMap: new Map()
|
||||
|
||||
property string mmsgFocusedTitle: ""
|
||||
property string mmsgFocusedAppId: ""
|
||||
property string currentKbLayout: ""
|
||||
|
||||
// Caches
|
||||
property var workspaceCache: ({})
|
||||
property var monitorScales: ({})
|
||||
property string lastWindowSig: ""
|
||||
// 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;
|
||||
|
||||
// Buffers
|
||||
property string streamBuffer: ""
|
||||
const rawList = ToplevelManager.toplevels.values;
|
||||
const finalList = [];
|
||||
let newFocusedIdx = -1;
|
||||
|
||||
// --- 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
|
||||
const currentObjects = new Set();
|
||||
const tagCapacities = new Map();
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (!line) continue;
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
if (totalCapacity === 0 && rawList.length > 0 && !internal.hasValidTagData)
|
||||
return;
|
||||
|
||||
// Overview Detection
|
||||
let activeCount = 0;
|
||||
for (let c = 0; c < seltags.length; c++) if (seltags[c] === '1') activeCount++;
|
||||
internal.multiTagState[outputName] = (activeCount > 1);
|
||||
const unassignedWindows = [];
|
||||
|
||||
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';
|
||||
// Pass 1: Known & Focused
|
||||
for (let i = 0; i < rawList.length; i++) {
|
||||
const toplevel = rawList[i];
|
||||
if (!toplevel || toplevel.outliers)
|
||||
continue;
|
||||
|
||||
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;
|
||||
}
|
||||
currentObjects.add(toplevel);
|
||||
|
||||
// 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];
|
||||
}
|
||||
const appId = toplevel.appId || toplevel.wayland.appId || "";
|
||||
const title = toplevel.title || toplevel.wayland.title || "";
|
||||
|
||||
// 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();
|
||||
}
|
||||
let outputName = root.selectedMonitor;
|
||||
if (toplevel.outputs && toplevel.outputs.length > 0) {
|
||||
outputName = toplevel.outputs[0].name;
|
||||
}
|
||||
|
||||
// --- WINDOW LIST MERGE ---
|
||||
function updateWindowList() {
|
||||
if (!ToplevelManager.toplevels) return;
|
||||
const currentActiveTag = internal.activeTags[outputName] || config.defaultWorkspaceId;
|
||||
const isMultiTag = internal.multiTagState[outputName] === true;
|
||||
let wsId = -1;
|
||||
|
||||
// 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 isFocused = toplevel.activated;
|
||||
const isMmsgFocus = (title === internal.mmsgFocusedTitle) && (appId === internal.mmsgFocusedAppId);
|
||||
|
||||
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<finalList.length; i++) {
|
||||
if (finalList[i].isFocused) newFocusedIdx = i;
|
||||
}
|
||||
|
||||
if (internal.windowStateMap.size > 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();
|
||||
}
|
||||
if (isFocused && isMmsgFocus && !isMultiTag) {
|
||||
wsId = currentActiveTag;
|
||||
internal.windowStateMap.set(toplevel, wsId);
|
||||
} else if (internal.windowStateMap.has(toplevel)) {
|
||||
wsId = internal.windowStateMap.get(toplevel);
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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();
|
||||
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 < finalList.length; i++) {
|
||||
if (finalList[i].isFocused)
|
||||
newFocusedIdx = i;
|
||||
}
|
||||
|
||||
if (internal.windowStateMap.size > 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();
|
||||
}
|
||||
}
|
||||
|
||||
// 4. PROCESSES
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
Process {
|
||||
id: eventStream
|
||||
running: false
|
||||
command: config.query.eventStream
|
||||
stdout: SplitParser {
|
||||
onRead: (line) => {
|
||||
// --- 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();
|
||||
}
|
||||
}
|
||||
|
||||
// 4. PROCESSES
|
||||
|
||||
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;
|
||||
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 = "";
|
||||
internal.processFrame(internal.streamBuffer);
|
||||
internal.streamBuffer = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
onExited: (code) => { if (code !== 0) restartTimer.start(); }
|
||||
}
|
||||
}
|
||||
Timer { id: restartTimer; interval: 1000; onTriggered: if(initialized) eventStream.running = true }
|
||||
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: 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) => {
|
||||
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;
|
||||
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(); }
|
||||
}
|
||||
}
|
||||
onExited: code => {
|
||||
if (code === 0)
|
||||
internal.updateScales();
|
||||
}
|
||||
}
|
||||
|
||||
// 5. WAYLAND & INIT
|
||||
// 5. WAYLAND & INIT
|
||||
|
||||
Connections {
|
||||
target: ToplevelManager
|
||||
function onToplevelsChanged() { internal.updateWindowList(); }
|
||||
}
|
||||
Timer {
|
||||
interval: 200; running: true; repeat: true
|
||||
onTriggered: internal.updateWindowList()
|
||||
Connections {
|
||||
target: ToplevelManager.toplevels
|
||||
function onValuesChanged() {
|
||||
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;
|
||||
Quickshell.execDetached(["mmsg", "-g", "-o"]);
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
function initialize() {
|
||||
if (initialized)
|
||||
return;
|
||||
Logger.i("MangoService", "Service Started");
|
||||
|
||||
function queryDisplayScales() { procOutputs.running = true; }
|
||||
procOutputs.running = true;
|
||||
procInitial.running = true;
|
||||
eventStream.running = true;
|
||||
Quickshell.execDetached(["mmsg", "-g", "-o"]);
|
||||
|
||||
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);
|
||||
}
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
function focusWindow(window) {
|
||||
if (window && window.handle) window.handle.activate();
|
||||
else if (window.workspaceId) switchToWorkspace({ id: window.workspaceId, output: window.output });
|
||||
}
|
||||
function queryDisplayScales() {
|
||||
procOutputs.running = true;
|
||||
}
|
||||
|
||||
function closeWindow(window) {
|
||||
if (window && window.handle) window.handle.close();
|
||||
else Quickshell.execDetached(config.action.killClient);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
function logout() { Quickshell.execDetached(config.action.quit); }
|
||||
function focusWindow(window) {
|
||||
if (window && window.handle)
|
||||
window.handle.activate();
|
||||
else if (window.workspaceId)
|
||||
switchToWorkspace({
|
||||
id: window.workspaceId,
|
||||
output: window.output
|
||||
});
|
||||
}
|
||||
|
||||
function closeWindow(window) {
|
||||
if (window && window.handle)
|
||||
window.handle.close();
|
||||
else
|
||||
Quickshell.execDetached(config.action.killClient);
|
||||
}
|
||||
|
||||
function logout() {
|
||||
Quickshell.execDetached(config.action.quit);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user