mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
295 lines
8.9 KiB
QML
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);
|
|
}
|
|
}
|
|
}
|