refactor(niri): use native C++ Niri IPC module instead of QML socket

This commit is contained in:
Lysec
2026-03-07 14:41:13 +01:00
parent 4d56a2aa99
commit d62eb5b249
+113 -355
View File
@@ -1,7 +1,6 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Niri
import Quickshell.Wayland
import qs.Commons import qs.Commons
import qs.Services.Keyboard import qs.Services.Keyboard
@@ -27,231 +26,117 @@ Item {
property var workspaceCache: ({}) property var workspaceCache: ({})
function initialize() { function initialize() {
niriEventStream.connected = true; Niri.refreshOutputs();
niriCommandSocket.connected = true; Niri.refreshWorkspaces();
Niri.refreshWindows();
startEventStream(); Qt.callLater(() => {
updateOutputs(); safeUpdateOutputs();
updateWorkspaces(); safeUpdateWorkspaces();
updateWindows(); safeUpdateWindows();
queryDisplayScales(); queryDisplayScales();
});
Logger.i("NiriService", "Service started"); Logger.i("NiriService", "Service started");
} }
// command from https://yalter.github.io/niri/niri_ipc/enum.Request.html // Connections to the C++ Niri IPC module
function sendSocketCommand(sock, command) { Connections {
sock.write(JSON.stringify(command) + "\n"); target: Niri
sock.flush(); 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() { function safeUpdateOutputs() {
sendSocketCommand(niriEventStream, "EventStream"); const niriOutputs = Niri.outputs.values;
}
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 = {};
outputCache = {}; outputCache = {};
for (const outputName in outputsData) { for (var i = 0; i < niriOutputs.length; i++) {
const output = outputsData[outputName]; const output = niriOutputs[i];
if (output && output.name) { outputCache[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, "name": output.name,
"connected": isConnected, "connected": output.connected,
"scale": logical.scale || 1.0, "scale": output.scale,
"width": logical.width || 0, "width": output.width,
"height": logical.height || 0, "height": output.height,
"x": logical.x || 0, "x": output.x,
"y": logical.y || 0, "y": output.y,
"physical_width": (output.physical_size && output.physical_size[0]) || 0, "physical_width": output.physicalWidth,
"physical_height": (output.physical_size && output.physical_size[1]) || 0, "physical_height": output.physicalHeight,
"refresh_rate": currentMode.refresh_rate || 0, "refresh_rate": output.refreshRate,
"vrr_supported": output.vrr_supported || false, "vrr_supported": output.vrrSupported,
"vrr_enabled": output.vrr_enabled || false, "vrr_enabled": output.vrrEnabled,
"transform": logical.transform || "Normal" "transform": output.transform
}; };
outputCache[output.name] = outputData;
scales[output.name] = outputData;
} }
} }
if (CompositorService && CompositorService.onDisplayScalesUpdated) { function safeUpdateWorkspaces() {
CompositorService.onDisplayScalesUpdated(scales); const niriWorkspaces = Niri.workspaces.values;
}
}
function recollectWorkspaces(workspacesData) {
const workspacesList = [];
workspaceCache = {}; workspaceCache = {};
for (const ws of workspacesData) { const workspacesList = [];
for (var i = 0; i < niriWorkspaces.length; i++) {
const ws = niriWorkspaces[i];
const wsData = { const wsData = {
"id": ws.id, "id": ws.id,
"idx": ws.idx, "idx": ws.idx,
"name": ws.name || "", "name": ws.name,
"output": ws.output || "", "output": ws.output,
"isFocused": ws.is_focused === true, "isFocused": ws.focused,
"isActive": ws.is_active === true, "isActive": ws.active,
"isUrgent": ws.is_urgent === true, "isUrgent": ws.urgent,
"isOccupied": ws.active_window_id ? true : false "isOccupied": ws.occupied
}; };
workspacesList.push(wsData); workspacesList.push(wsData);
workspaceCache[ws.id] = wsData; workspaceCache[ws.id] = wsData;
} }
workspacesList.sort((a, b) => { // Workspaces come pre-sorted from C++ (by output then idx)
if (a.output !== b.output) {
return a.output.localeCompare(b.output);
}
return a.idx - b.idx;
});
workspaces.clear(); workspaces.clear();
for (var i = 0; i < workspacesList.length; i++) { for (var j = 0; j < workspacesList.length; j++) {
workspaces.append(workspacesList[i]); workspaces.append(workspacesList[j]);
}
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
};
} }
} }
function getWindowOutput(win) { function getWindowOutput(win) {
for (var i = 0; i < workspaces.count; i++) { 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 workspaces.get(i).output;
} }
} }
return null; 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) { function toSortedWindowList(windowList) {
return windowList.map(win => { return windowList.map(win => {
const workspace = workspaceCache[win.workspaceId]; const workspace = workspaceCache[win.workspaceId];
@@ -287,15 +172,31 @@ Item {
}).map(info => info.window); }).map(info => info.window);
} }
function recollectWindows(windowsData) { function safeUpdateWindows() {
const niriWindows = Niri.windows.values;
const windowsList = []; 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; focusedWindowIndex = -1;
for (var i = 0; i < windows.length; i++) { for (var i = 0; i < windows.length; i++) {
if (windows[i].isFocused) { if (windows[i].isFocused) {
@@ -303,154 +204,17 @@ Item {
break; break;
} }
} }
activeWindowChanged();
} }
function handleWindowOpenedOrChanged(eventData) { function queryDisplayScales() {
try { if (CompositorService && CompositorService.onDisplayScalesUpdated) {
const windowData = eventData.window; CompositorService.onDisplayScalesUpdated(outputCache);
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 switchToWorkspace(workspace) { function switchToWorkspace(workspace) {
try { try {
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspace.idx.toString()]); Niri.dispatch(["focus-workspace", workspace.idx.toString()]);
} catch (e) { } catch (e) {
Logger.e("NiriService", "Failed to switch workspace:", e); Logger.e("NiriService", "Failed to switch workspace:", e);
} }
@@ -459,7 +223,7 @@ Item {
function scrollWorkspaceContent(direction) { function scrollWorkspaceContent(direction) {
try { try {
var action = direction < 0 ? "focus-column-left" : "focus-column-right"; var action = direction < 0 ? "focus-column-left" : "focus-column-right";
Quickshell.execDetached(["niri", "msg", "action", action]); Niri.dispatch([action]);
} catch (e) { } catch (e) {
Logger.e("NiriService", "Failed to scroll workspace content:", e); Logger.e("NiriService", "Failed to scroll workspace content:", e);
} }
@@ -467,7 +231,7 @@ Item {
function focusWindow(window) { function focusWindow(window) {
try { try {
Quickshell.execDetached(["niri", "msg", "action", "focus-window", "--id", window.id.toString()]); Niri.dispatch(["focus-window", "--id", window.id.toString()]);
} catch (e) { } catch (e) {
Logger.e("NiriService", "Failed to switch window:", e); Logger.e("NiriService", "Failed to switch window:", e);
} }
@@ -475,7 +239,7 @@ Item {
function closeWindow(window) { function closeWindow(window) {
try { try {
Quickshell.execDetached(["niri", "msg", "action", "close-window", "--id", window.id.toString()]); Niri.dispatch(["close-window", "--id", window.id.toString()]);
} catch (e) { } catch (e) {
Logger.e("NiriService", "Failed to close window:", e); Logger.e("NiriService", "Failed to close window:", e);
} }
@@ -483,7 +247,7 @@ Item {
function turnOffMonitors() { function turnOffMonitors() {
try { try {
Quickshell.execDetached(["niri", "msg", "action", "power-off-monitors"]); Niri.dispatch(["power-off-monitors"]);
} catch (e) { } catch (e) {
Logger.e("NiriService", "Failed to turn off monitors:", e); Logger.e("NiriService", "Failed to turn off monitors:", e);
} }
@@ -491,7 +255,7 @@ Item {
function turnOnMonitors() { function turnOnMonitors() {
try { try {
Quickshell.execDetached(["niri", "msg", "action", "power-on-monitors"]); Niri.dispatch(["power-on-monitors"]);
} catch (e) { } catch (e) {
Logger.e("NiriService", "Failed to turn on monitors:", e); Logger.e("NiriService", "Failed to turn on monitors:", e);
} }
@@ -499,7 +263,7 @@ Item {
function logout() { function logout() {
try { try {
Quickshell.execDetached(["niri", "msg", "action", "quit", "--skip-confirmation"]); Niri.dispatch(["quit", "--skip-confirmation"]);
} catch (e) { } catch (e) {
Logger.e("NiriService", "Failed to logout:", e); Logger.e("NiriService", "Failed to logout:", e);
} }
@@ -507,7 +271,7 @@ Item {
function cycleKeyboardLayout() { function cycleKeyboardLayout() {
try { try {
Quickshell.execDetached(["niri", "msg", "action", "switch-layout", "next"]); Niri.dispatch(["switch-layout", "next"]);
} catch (e) { } catch (e) {
Logger.e("NiriService", "Failed to cycle keyboard layout:", e); Logger.e("NiriService", "Failed to cycle keyboard layout:", e);
} }
@@ -516,19 +280,13 @@ Item {
function getFocusedScreen() { function getFocusedScreen() {
// On niri the code below only works when you have an actual app selected on that screen. // On niri the code below only works when you have an actual app selected on that screen.
return null; return null;
// const activeToplevel = ToplevelManager.activeToplevel;
// if (activeToplevel && activeToplevel.screens && activeToplevel.screens.length > 0) {
// return activeToplevel.screens[0];
// }
// return null;
} }
function spawn(command) { function spawn(command) {
try { try {
const niriCommand = ["niri", "msg", "action", "spawn", "--"].concat(command); const niriArgs = ["spawn", "--"].concat(command);
Logger.d("NiriService", "Calling niri spawn: " + niriCommand.join(" ")); Logger.d("NiriService", "Calling niri spawn: niri msg action " + niriArgs.join(" "));
Quickshell.execDetached(niriCommand); Niri.dispatch(niriArgs);
} catch (e) { } catch (e) {
Logger.e("NiriService", "Failed to spawn command:", e); Logger.e("NiriService", "Failed to spawn command:", e);
} }