From d62eb5b249d8166fbcc549dc7504f26b1e024672 Mon Sep 17 00:00:00 2001 From: Lysec Date: Sat, 7 Mar 2026 14:41:13 +0100 Subject: [PATCH] refactor(niri): use native C++ Niri IPC module instead of QML socket --- Services/Compositor/NiriService.qml | 474 +++++++--------------------- 1 file changed, 116 insertions(+), 358 deletions(-) diff --git a/Services/Compositor/NiriService.qml b/Services/Compositor/NiriService.qml index f4bbaaf94..a74efb0ed 100644 --- a/Services/Compositor/NiriService.qml +++ b/Services/Compositor/NiriService.qml @@ -1,7 +1,6 @@ import QtQuick import Quickshell -import Quickshell.Io -import Quickshell.Wayland +import Quickshell.Niri import qs.Commons import qs.Services.Keyboard @@ -27,231 +26,117 @@ Item { property var workspaceCache: ({}) function initialize() { - niriEventStream.connected = true; - niriCommandSocket.connected = true; + Niri.refreshOutputs(); + Niri.refreshWorkspaces(); + Niri.refreshWindows(); + + Qt.callLater(() => { + safeUpdateOutputs(); + safeUpdateWorkspaces(); + safeUpdateWindows(); + queryDisplayScales(); + }); - startEventStream(); - updateOutputs(); - updateWorkspaces(); - updateWindows(); - queryDisplayScales(); Logger.i("NiriService", "Service started"); } - // command from https://yalter.github.io/niri/niri_ipc/enum.Request.html - function sendSocketCommand(sock, command) { - sock.write(JSON.stringify(command) + "\n"); - sock.flush(); + // 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 startEventStream() { - sendSocketCommand(niriEventStream, "EventStream"); - } - - function updateOutputs() { - sendSocketCommand(niriCommandSocket, "Outputs"); - } - - function updateWorkspaces() { - sendSocketCommand(niriCommandSocket, "Workspaces"); - } - - function updateWindows() { - sendSocketCommand(niriCommandSocket, "Windows"); - } - - Timer { - id: workspaceUpdateTimer - interval: 50 - repeat: false - onTriggered: updateWorkspaces() - } - - function queryDisplayScales() { - sendSocketCommand(niriCommandSocket, "Outputs"); - } - - function recollectOutputs(outputsData) { - const scales = {}; + function safeUpdateOutputs() { + const niriOutputs = Niri.outputs.values; outputCache = {}; - for (const outputName in outputsData) { - const output = outputsData[outputName]; - if (output && output.name) { - const isConnected = output.logical !== null && output.current_mode !== null; - const logical = output.logical || {}; - const currentModeIdx = output.current_mode ?? 0; - const modes = output.modes || []; - const currentMode = modes[currentModeIdx] || {}; - - const outputData = { - "name": output.name, - "connected": isConnected, - "scale": logical.scale || 1.0, - "width": logical.width || 0, - "height": logical.height || 0, - "x": logical.x || 0, - "y": logical.y || 0, - "physical_width": (output.physical_size && output.physical_size[0]) || 0, - "physical_height": (output.physical_size && output.physical_size[1]) || 0, - "refresh_rate": currentMode.refresh_rate || 0, - "vrr_supported": output.vrr_supported || false, - "vrr_enabled": output.vrr_enabled || false, - "transform": logical.transform || "Normal" - }; - - outputCache[output.name] = outputData; - scales[output.name] = outputData; - } - } - - if (CompositorService && CompositorService.onDisplayScalesUpdated) { - CompositorService.onDisplayScalesUpdated(scales); + 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 recollectWorkspaces(workspacesData) { - const workspacesList = []; + function safeUpdateWorkspaces() { + const niriWorkspaces = Niri.workspaces.values; workspaceCache = {}; - for (const ws of workspacesData) { + 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.is_focused === true, - "isActive": ws.is_active === true, - "isUrgent": ws.is_urgent === true, - "isOccupied": ws.active_window_id ? true : false + "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; } - workspacesList.sort((a, b) => { - if (a.output !== b.output) { - return a.output.localeCompare(b.output); - } - return a.idx - b.idx; - }); - + // Workspaces come pre-sorted from C++ (by output then idx) workspaces.clear(); - for (var i = 0; i < workspacesList.length; i++) { - workspaces.append(workspacesList[i]); - } - - workspaceChanged(); - } - - Socket { - id: niriCommandSocket - path: Quickshell.env("NIRI_SOCKET") - connected: false - - parser: SplitParser { - onRead: function (line) { - try { - const data = JSON.parse(line); - - if (data && data.Ok) { - const res = data.Ok; - if (res.Windows) { - recollectWindows(res.Windows); - } else if (res.Outputs) { - recollectOutputs(res.Outputs); - } else if (res.Workspaces) { - recollectWorkspaces(res.Workspaces); - } - } else { - Logger.e("NiriService", "Niri returned an error:", data.Err, line); - } - } catch (e) { - Logger.e("NiriService", "Failed to parse data from socket:", e, line); - return; - } - } - } - } - - Socket { - id: niriEventStream - path: Quickshell.env("NIRI_SOCKET") - connected: false - - parser: SplitParser { - onRead: data => { - try { - const event = JSON.parse(data.trim()); - - if (event.WorkspacesChanged) { - recollectWorkspaces(event.WorkspacesChanged.workspaces); - } else if (event.WindowOpenedOrChanged) { - handleWindowOpenedOrChanged(event.WindowOpenedOrChanged); - } else if (event.WindowClosed) { - handleWindowClosed(event.WindowClosed); - } else if (event.WindowsChanged) { - handleWindowsChanged(event.WindowsChanged); - } else if (event.WorkspaceActivated) { - workspaceUpdateTimer.restart(); - } else if (event.WindowFocusChanged) { - handleWindowFocusChanged(event.WindowFocusChanged); - } else if (event.WindowLayoutsChanged) { - handleWindowLayoutsChanged(event.WindowLayoutsChanged); - } else if (event.OverviewOpenedOrClosed) { - handleOverviewOpenedOrClosed(event.OverviewOpenedOrClosed); - } else if (event.OutputsChanged) { - queryDisplayScales(); - } else if (event.ConfigLoaded) { - queryDisplayScales(); - } else if (event.KeyboardLayoutsChanged) { - handleKeyboardLayoutsChanged(event.KeyboardLayoutsChanged); - } else if (event.KeyboardLayoutSwitched) { - handleKeyboardLayoutSwitched(event.KeyboardLayoutSwitched); - } - } catch (e) { - Logger.e("NiriService", "Error parsing event stream:", e, data); - } - } - } - } - - function getWindowPosition(layout) { - if (layout.pos_in_scrolling_layout) { - return { - "x": layout.pos_in_scrolling_layout[0], - "y": layout.pos_in_scrolling_layout[1] - }; - } else { - return { - "x": floatingWindowPosition, - "y": floatingWindowPosition - }; + 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.workspace_id) { + if (workspaces.get(i).id === win.workspaceId) { return workspaces.get(i).output; } } return null; } - function getWindowData(win) { - return { - "id": win.id, - "title": win.title || "", - "appId": win.app_id || "", - "workspaceId": win.workspace_id || -1, - "isFocused": win.is_focused === true, - "output": getWindowOutput(win) || "", - "position": getWindowPosition(win.layout) - }; - } - function toSortedWindowList(windowList) { return windowList.map(win => { const workspace = workspaceCache[win.workspaceId]; @@ -287,15 +172,31 @@ Item { }).map(info => info.window); } - function recollectWindows(windowsData) { + function safeUpdateWindows() { + const niriWindows = Niri.windows.values; const windowsList = []; - for (const win of windowsData) { - windowsList.push(getWindowData(win)); - } - windows = toSortedWindowList(windowsList); - windowListChanged(); - // Find focused window index in the SORTED windows array + 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) { @@ -303,154 +204,17 @@ Item { break; } } - activeWindowChanged(); } - function handleWindowOpenedOrChanged(eventData) { - try { - const windowData = eventData.window; - const existingIndex = windows.findIndex(w => w.id === windowData.id); - const newWindow = getWindowData(windowData); - - // Find the previously focused window ID before any modifications - const previouslyFocusedId = focusedWindowIndex >= 0 && focusedWindowIndex < windows.length ? windows[focusedWindowIndex].id : null; - - if (existingIndex >= 0) { - windows[existingIndex] = newWindow; - } else { - windows.push(newWindow); - } - windows = toSortedWindowList(windows); - - if (newWindow.isFocused) { - focusedWindowIndex = windows.findIndex(w => w.id === windowData.id); - - // Clear focus on the previously focused window by ID (not index, since list was re-sorted) - if (previouslyFocusedId !== null && previouslyFocusedId !== windowData.id) { - const oldFocusedWindow = windows.find(w => w.id === previouslyFocusedId); - if (oldFocusedWindow) { - oldFocusedWindow.isFocused = false; - } - } - activeWindowChanged(); - } - - windowListChanged(); - workspaceUpdateTimer.restart(); - } catch (e) { - Logger.e("NiriService", "Error handling WindowOpenedOrChanged:", e); - } - } - - function handleWindowClosed(eventData) { - try { - const windowId = eventData.id; - const windowIndex = windows.findIndex(w => w.id === windowId); - - if (windowIndex >= 0) { - if (windowIndex === focusedWindowIndex) { - focusedWindowIndex = -1; - activeWindowChanged(); - } else if (focusedWindowIndex > windowIndex) { - focusedWindowIndex--; - } - - windows.splice(windowIndex, 1); - windowListChanged(); - workspaceUpdateTimer.restart(); - } - } catch (e) { - Logger.e("NiriService", "Error handling WindowClosed:", e); - } - } - - function handleWindowsChanged(eventData) { - try { - const windowsData = eventData.windows; - recollectWindows(windowsData); - } catch (e) { - Logger.e("NiriService", "Error handling WindowsChanged:", e); - } - } - - function handleWindowFocusChanged(eventData) { - try { - const focusedId = eventData.id; - - if (windows[focusedWindowIndex]) { - windows[focusedWindowIndex].isFocused = false; - } - - if (focusedId) { - const newIndex = windows.findIndex(w => w.id === focusedId); - - if (newIndex >= 0 && newIndex < windows.length) { - windows[newIndex].isFocused = true; - } - - focusedWindowIndex = newIndex >= 0 ? newIndex : -1; - } else { - focusedWindowIndex = -1; - } - - activeWindowChanged(); - } catch (e) { - Logger.e("NiriService", "Error handling WindowFocusChanged:", e); - } - } - - function handleWindowLayoutsChanged(eventData) { - try { - for (const change of eventData.changes) { - const windowId = change[0]; - const layout = change[1]; - const window = windows.find(w => w.id === windowId); - if (window) { - window.position = getWindowPosition(layout); - } - } - - windows = toSortedWindowList(windows); - - windowListChanged(); - } catch (e) { - Logger.e("NiriService", "Error handling WindowLayoutChanged:", e); - } - } - - function handleOverviewOpenedOrClosed(eventData) { - try { - overviewActive = eventData.is_open; - Logger.d("NiriService", "Overview opened or closed:", eventData.is_open); - } catch (e) { - Logger.e("NiriService", "Error handling OverviewOpenedOrClosed:", e); - } - } - - function handleKeyboardLayoutsChanged(eventData) { - try { - keyboardLayouts = eventData.keyboard_layouts.names; - const layoutName = keyboardLayouts[eventData.keyboard_layouts.current_idx]; - KeyboardLayoutService.setCurrentLayout(layoutName); - Logger.d("NiriService", "Keyboard layouts changed:", keyboardLayouts.toString()); - } catch (e) { - Logger.e("NiriService", "Error handling keyboardLayoutsChanged:", e); - } - } - - function handleKeyboardLayoutSwitched(eventData) { - try { - const layoutName = keyboardLayouts[eventData.idx]; - KeyboardLayoutService.setCurrentLayout(layoutName); - Logger.d("NiriService", "Keyboard layout switched:", layoutName); - } catch (e) { - Logger.e("NiriService", "Error handling KeyboardLayoutSwitched:", e); + function queryDisplayScales() { + if (CompositorService && CompositorService.onDisplayScalesUpdated) { + CompositorService.onDisplayScalesUpdated(outputCache); } } function switchToWorkspace(workspace) { try { - Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspace.idx.toString()]); + Niri.dispatch(["focus-workspace", workspace.idx.toString()]); } catch (e) { Logger.e("NiriService", "Failed to switch workspace:", e); } @@ -459,7 +223,7 @@ Item { function scrollWorkspaceContent(direction) { try { var action = direction < 0 ? "focus-column-left" : "focus-column-right"; - Quickshell.execDetached(["niri", "msg", "action", action]); + Niri.dispatch([action]); } catch (e) { Logger.e("NiriService", "Failed to scroll workspace content:", e); } @@ -467,7 +231,7 @@ Item { function focusWindow(window) { try { - Quickshell.execDetached(["niri", "msg", "action", "focus-window", "--id", window.id.toString()]); + Niri.dispatch(["focus-window", "--id", window.id.toString()]); } catch (e) { Logger.e("NiriService", "Failed to switch window:", e); } @@ -475,7 +239,7 @@ Item { function closeWindow(window) { try { - Quickshell.execDetached(["niri", "msg", "action", "close-window", "--id", window.id.toString()]); + Niri.dispatch(["close-window", "--id", window.id.toString()]); } catch (e) { Logger.e("NiriService", "Failed to close window:", e); } @@ -483,7 +247,7 @@ Item { function turnOffMonitors() { try { - Quickshell.execDetached(["niri", "msg", "action", "power-off-monitors"]); + Niri.dispatch(["power-off-monitors"]); } catch (e) { Logger.e("NiriService", "Failed to turn off monitors:", e); } @@ -491,7 +255,7 @@ Item { function turnOnMonitors() { try { - Quickshell.execDetached(["niri", "msg", "action", "power-on-monitors"]); + Niri.dispatch(["power-on-monitors"]); } catch (e) { Logger.e("NiriService", "Failed to turn on monitors:", e); } @@ -499,7 +263,7 @@ Item { function logout() { try { - Quickshell.execDetached(["niri", "msg", "action", "quit", "--skip-confirmation"]); + Niri.dispatch(["quit", "--skip-confirmation"]); } catch (e) { Logger.e("NiriService", "Failed to logout:", e); } @@ -507,7 +271,7 @@ Item { function cycleKeyboardLayout() { try { - Quickshell.execDetached(["niri", "msg", "action", "switch-layout", "next"]); + Niri.dispatch(["switch-layout", "next"]); } catch (e) { Logger.e("NiriService", "Failed to cycle keyboard layout:", e); } @@ -516,19 +280,13 @@ Item { function getFocusedScreen() { // On niri the code below only works when you have an actual app selected on that screen. return null; - - // const activeToplevel = ToplevelManager.activeToplevel; - // if (activeToplevel && activeToplevel.screens && activeToplevel.screens.length > 0) { - // return activeToplevel.screens[0]; - // } - // return null; } function spawn(command) { try { - const niriCommand = ["niri", "msg", "action", "spawn", "--"].concat(command); - Logger.d("NiriService", "Calling niri spawn: " + niriCommand.join(" ")); - Quickshell.execDetached(niriCommand); + 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); }