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); } } }