Files
noctalia-shell/Services/Compositor/NiriService.qml
T

295 lines
8.9 KiB
QML

import QtQuick
import Quickshell
import Quickshell.Niri
import qs.Commons
import qs.Services.Keyboard
Item {
id: root
property int floatingWindowPosition: Number.MAX_SAFE_INTEGER
property ListModel workspaces: ListModel {}
property var windows: []
property int focusedWindowIndex: -1
property bool overviewActive: false
property var keyboardLayouts: []
signal workspaceChanged
signal activeWindowChanged
signal windowListChanged
signal displayScalesChanged
property var outputCache: ({})
property var workspaceCache: ({})
function initialize() {
Niri.refreshOutputs();
Niri.refreshWorkspaces();
Niri.refreshWindows();
Qt.callLater(() => {
safeUpdateOutputs();
safeUpdateWorkspaces();
safeUpdateWindows();
queryDisplayScales();
});
Logger.i("NiriService", "Service started");
}
// Connections to the C++ Niri IPC module
Connections {
target: Niri
function onWorkspacesUpdated() {
safeUpdateWorkspaces();
workspaceChanged();
}
function onWindowsUpdated() {
safeUpdateWindows();
windowListChanged();
activeWindowChanged();
}
function onOutputsUpdated() {
safeUpdateOutputs();
queryDisplayScales();
}
function onOverviewActiveChanged() {
overviewActive = Niri.overviewActive;
}
function onKeyboardLayoutsChanged() {
keyboardLayouts = Niri.keyboardLayoutNames;
const layoutName = Niri.currentKeyboardLayoutName;
if (layoutName) {
KeyboardLayoutService.setCurrentLayout(layoutName);
}
Logger.d("NiriService", "Keyboard layouts changed:", keyboardLayouts.toString());
}
function onKeyboardLayoutSwitched() {
const layoutName = Niri.currentKeyboardLayoutName;
if (layoutName) {
KeyboardLayoutService.setCurrentLayout(layoutName);
}
Logger.d("NiriService", "Keyboard layout switched:", layoutName);
}
}
function safeUpdateOutputs() {
const niriOutputs = Niri.outputs.values;
outputCache = {};
for (var i = 0; i < niriOutputs.length; i++) {
const output = niriOutputs[i];
outputCache[output.name] = {
"name": output.name,
"connected": output.connected,
"scale": output.scale,
"width": output.width,
"height": output.height,
"x": output.x,
"y": output.y,
"physical_width": output.physicalWidth,
"physical_height": output.physicalHeight,
"refresh_rate": output.refreshRate,
"vrr_supported": output.vrrSupported,
"vrr_enabled": output.vrrEnabled,
"transform": output.transform
};
}
}
function safeUpdateWorkspaces() {
const niriWorkspaces = Niri.workspaces.values;
workspaceCache = {};
const workspacesList = [];
for (var i = 0; i < niriWorkspaces.length; i++) {
const ws = niriWorkspaces[i];
const wsData = {
"id": ws.id,
"idx": ws.idx,
"name": ws.name,
"output": ws.output,
"isFocused": ws.focused,
"isActive": ws.active,
"isUrgent": ws.urgent,
"isOccupied": ws.occupied
};
workspacesList.push(wsData);
workspaceCache[ws.id] = wsData;
}
// Workspaces come pre-sorted from C++ (by output then idx)
workspaces.clear();
for (var j = 0; j < workspacesList.length; j++) {
workspaces.append(workspacesList[j]);
}
}
function getWindowOutput(win) {
for (var i = 0; i < workspaces.count; i++) {
if (workspaces.get(i).id === win.workspaceId) {
return workspaces.get(i).output;
}
}
return null;
}
function toSortedWindowList(windowList) {
return windowList.map(win => {
const workspace = workspaceCache[win.workspaceId];
const output = (workspace && workspace.output) ? outputCache[workspace.output] : null;
return {
window: win,
workspaceIdx: workspace ? workspace.idx : 0,
outputX: output ? output.x : 0,
outputY: output ? output.y : 0
};
}).sort((a, b) => {
// Sort by output position first
if (a.outputX !== b.outputX) {
return a.outputX - b.outputX;
}
if (a.outputY !== b.outputY) {
return a.outputY - b.outputY;
}
// Then by workspace index
if (a.workspaceIdx !== b.workspaceIdx) {
return a.workspaceIdx - b.workspaceIdx;
}
// Then by window position
if (a.window.position.x !== b.window.position.x) {
return a.window.position.x - b.window.position.x;
}
if (a.window.position.y !== b.window.position.y) {
return a.window.position.y - b.window.position.y;
}
// Finally by window ID to ensure consistent ordering
return a.window.id - b.window.id;
}).map(info => info.window);
}
function safeUpdateWindows() {
const niriWindows = Niri.windows.values;
const windowsList = [];
for (var i = 0; i < niriWindows.length; i++) {
const win = niriWindows[i];
windowsList.push({
"id": win.id,
"title": win.title || "",
"appId": win.appId || "",
"workspaceId": win.workspaceId || -1,
"isFocused": win.focused,
"output": win.output || getWindowOutput(win) || "",
"position": {
"x": win.isFloating ? floatingWindowPosition : win.positionX,
"y": win.isFloating ? floatingWindowPosition : win.positionY
}
});
}
windows = toSortedWindowList(windowsList);
safeUpdateFocusedWindow();
}
function safeUpdateFocusedWindow() {
focusedWindowIndex = -1;
for (var i = 0; i < windows.length; i++) {
if (windows[i].isFocused) {
focusedWindowIndex = i;
break;
}
}
}
function queryDisplayScales() {
if (CompositorService && CompositorService.onDisplayScalesUpdated) {
CompositorService.onDisplayScalesUpdated(outputCache);
}
}
function switchToWorkspace(workspace) {
try {
Niri.dispatch(["focus-workspace", workspace.idx.toString()]);
} catch (e) {
Logger.e("NiriService", "Failed to switch workspace:", e);
}
}
function scrollWorkspaceContent(direction) {
try {
var action = direction < 0 ? "focus-column-left" : "focus-column-right";
Niri.dispatch([action]);
} catch (e) {
Logger.e("NiriService", "Failed to scroll workspace content:", e);
}
}
function focusWindow(window) {
try {
Niri.dispatch(["focus-window", "--id", window.id.toString()]);
} catch (e) {
Logger.e("NiriService", "Failed to switch window:", e);
}
}
function closeWindow(window) {
try {
Niri.dispatch(["close-window", "--id", window.id.toString()]);
} catch (e) {
Logger.e("NiriService", "Failed to close window:", e);
}
}
function turnOffMonitors() {
try {
Niri.dispatch(["power-off-monitors"]);
} catch (e) {
Logger.e("NiriService", "Failed to turn off monitors:", e);
}
}
function turnOnMonitors() {
try {
Niri.dispatch(["power-on-monitors"]);
} catch (e) {
Logger.e("NiriService", "Failed to turn on monitors:", e);
}
}
function logout() {
try {
Niri.dispatch(["quit", "--skip-confirmation"]);
} catch (e) {
Logger.e("NiriService", "Failed to logout:", e);
}
}
function cycleKeyboardLayout() {
try {
Niri.dispatch(["switch-layout", "next"]);
} catch (e) {
Logger.e("NiriService", "Failed to cycle keyboard layout:", e);
}
}
function getFocusedScreen() {
// On niri the code below only works when you have an actual app selected on that screen.
return null;
}
function spawn(command) {
try {
const niriArgs = ["spawn", "--"].concat(command);
Logger.d("NiriService", "Calling niri spawn: niri msg action " + niriArgs.join(" "));
Niri.dispatch(niriArgs);
} catch (e) {
Logger.e("NiriService", "Failed to spawn command:", e);
}
}
}