summaryrefslogtreecommitdiff
path: root/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Services/NiriService.qml
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Services/NiriService.qml')
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Services/NiriService.qml1494
1 files changed, 1494 insertions, 0 deletions
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Services/NiriService.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Services/NiriService.qml
new file mode 100644
index 0000000..49748eb
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Services/NiriService.qml
@@ -0,0 +1,1494 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import QtCore
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import qs.Common
+
+Singleton {
+ id: root
+
+ readonly property string socketPath: Quickshell.env("NIRI_SOCKET")
+
+ property var workspaces: ({})
+ property var allWorkspaces: []
+ property int focusedWorkspaceIndex: 0
+ property string focusedWorkspaceId: ""
+ property var currentOutputWorkspaces: []
+ property string currentOutput: ""
+
+ property var outputs: ({})
+ property var windows: []
+ property var displayScales: ({})
+
+ property var _realOutputs: ({})
+
+ property bool inOverview: false
+
+ property var casts: []
+ property bool hasCasts: casts.length > 0
+ property bool hasActiveCast: casts.some(c => c.is_active)
+
+ property int currentKeyboardLayoutIndex: 0
+ property var keyboardLayoutNames: []
+
+ property string configValidationOutput: ""
+ property bool hasInitialConnection: false
+ property bool suppressConfigToast: true
+ property bool suppressNextConfigToast: false
+ property bool matugenSuppression: false
+ property bool configGenerationPending: false
+
+ readonly property string screenshotsDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.PicturesLocation)) + "/Screenshots"
+ property string pendingScreenshotPath: ""
+
+ signal windowUrgentChanged
+ signal configReloaded
+
+ function setWorkspaces(newMap) {
+ root.workspaces = newMap;
+ root.allWorkspaces = Object.values(newMap).sort((a, b) => a.idx - b.idx);
+ }
+
+ function validate() {
+ validateProcess.running = true;
+ }
+
+ Component.onCompleted: fetchOutputs()
+
+ Timer {
+ id: suppressToastTimer
+ interval: 3000
+ onTriggered: root.suppressConfigToast = false
+ }
+
+ Timer {
+ id: suppressResetTimer
+ interval: 5000
+ onTriggered: root.matugenSuppression = false
+ }
+
+ Timer {
+ id: configGenerationDebounce
+ interval: 100
+ onTriggered: root.doGenerateNiriLayoutConfig()
+ }
+
+ property int _lastGapValue: -1
+
+ Connections {
+ target: SettingsData
+ function onBarConfigsChanged() {
+ const newGaps = Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4));
+ if (newGaps === root._lastGapValue)
+ return;
+ root._lastGapValue = newGaps;
+ generateNiriLayoutConfig();
+ }
+ }
+
+ Process {
+ id: validateProcess
+ command: ["niri", "validate"]
+ running: false
+
+ stderr: StdioCollector {
+ onStreamFinished: {
+ const lines = text.split('\n');
+ const trimmedLines = lines.map(line => line.replace(/\s+$/, '')).filter(line => line.length > 0);
+ configValidationOutput = trimmedLines.join('\n').trim();
+ }
+ }
+
+ onExited: exitCode => {
+ if (exitCode === 0) {
+ configValidationOutput = "";
+ } else if (hasInitialConnection && configValidationOutput.length > 0) {
+ ToastService.showError("niri: failed to load config", configValidationOutput, "", "niri-config");
+ }
+ }
+ }
+
+ Process {
+ id: writeConfigProcess
+ property string configContent: ""
+ property string configPath: ""
+
+ onExited: exitCode => {
+ if (exitCode === 0) {
+ console.info("NiriService: Generated layout config at", configPath);
+ return;
+ }
+ console.warn("NiriService: Failed to write layout config, exit code:", exitCode);
+ }
+ }
+
+ Process {
+ id: writeAlttabProcess
+ property string alttabContent: ""
+ property string alttabPath: ""
+
+ onExited: exitCode => {
+ if (exitCode === 0) {
+ console.info("NiriService: Generated alttab config at", alttabPath);
+ return;
+ }
+ console.warn("NiriService: Failed to write alttab config, exit code:", exitCode);
+ }
+ }
+
+ Process {
+ id: writeBlurruleProcess
+ property string blurrulePath: ""
+
+ onExited: exitCode => {
+ if (exitCode === 0) {
+ console.info("NiriService: Generated wpblur config at", blurrulePath);
+ return;
+ }
+ console.warn("NiriService: Failed to write wpblur config, exit code:", exitCode);
+ }
+ }
+
+ Process {
+ id: writeCursorProcess
+ property string cursorContent: ""
+ property string cursorPath: ""
+
+ onExited: exitCode => {
+ if (exitCode === 0) {
+ console.info("NiriService: Generated cursor config at", cursorPath);
+ return;
+ }
+ console.warn("NiriService: Failed to write cursor config, exit code:", exitCode);
+ }
+ }
+
+ DankSocket {
+ id: eventStreamSocket
+ path: root.socketPath
+ connected: CompositorService.isNiri
+
+ onConnectionStateChanged: {
+ if (connected) {
+ send('"EventStream"');
+ fetchOutputs();
+ }
+ }
+
+ parser: SplitParser {
+ onRead: line => {
+ try {
+ const event = JSON.parse(line);
+ handleNiriEvent(event);
+ } catch (e) {
+ console.warn("NiriService: Failed to parse event:", line, e);
+ }
+ }
+ }
+ }
+
+ DankSocket {
+ id: requestSocket
+ path: root.socketPath
+ connected: CompositorService.isNiri
+ }
+
+ function fetchOutputs() {
+ if (!CompositorService.isNiri)
+ return;
+ Proc.runCommand("niri-fetch-outputs", ["niri", "msg", "-j", "outputs"], (output, exitCode) => {
+ if (exitCode !== 0) {
+ console.warn("NiriService: Failed to fetch outputs, exit code:", exitCode);
+ return;
+ }
+ try {
+ const outputsData = JSON.parse(output);
+ outputs = outputsData;
+ console.info("NiriService: Loaded", Object.keys(outputsData).length, "outputs");
+ updateDisplayScales();
+ if (windows.length > 0) {
+ windows = sortWindowsByLayout(windows);
+ }
+ } catch (e) {
+ console.warn("NiriService: Failed to parse outputs:", e);
+ }
+ });
+ }
+
+ function updateDisplayScales() {
+ if (!outputs || Object.keys(outputs).length === 0)
+ return;
+ const scales = {};
+ for (const outputName in outputs) {
+ const output = outputs[outputName];
+ if (output.logical && output.logical.scale !== undefined) {
+ scales[outputName] = output.logical.scale;
+ }
+ }
+
+ displayScales = scales;
+ }
+
+ function sortWindowsByLayout(windowList) {
+ const enriched = windowList.map(w => {
+ const ws = workspaces[w.workspace_id];
+ if (!ws) {
+ return {
+ "window": w,
+ "outputX": 999999,
+ "outputY": 999999,
+ "wsIdx": 999999,
+ "col": 999999,
+ "row": 999999
+ };
+ }
+
+ const outputInfo = outputs[ws.output];
+ const outputX = (outputInfo && outputInfo.logical) ? outputInfo.logical.x : 999999;
+ const outputY = (outputInfo && outputInfo.logical) ? outputInfo.logical.y : 999999;
+
+ const pos = w.layout?.pos_in_scrolling_layout;
+ const col = (pos && pos.length >= 2) ? pos[0] : 999999;
+ const row = (pos && pos.length >= 2) ? pos[1] : 999999;
+
+ return {
+ "window": w,
+ "outputX": outputX,
+ "outputY": outputY,
+ "wsIdx": ws.idx,
+ "col": col,
+ "row": row
+ };
+ });
+
+ enriched.sort((a, b) => {
+ if (a.outputX !== b.outputX)
+ return a.outputX - b.outputX;
+ if (a.outputY !== b.outputY)
+ return a.outputY - b.outputY;
+ if (a.wsIdx !== b.wsIdx)
+ return a.wsIdx - b.wsIdx;
+ if (a.col !== b.col)
+ return a.col - b.col;
+ if (a.row !== b.row)
+ return a.row - b.row;
+ return a.window.id - b.window.id;
+ });
+
+ return enriched.map(e => e.window);
+ }
+
+ function handleNiriEvent(event) {
+ const eventType = Object.keys(event)[0];
+
+ switch (eventType) {
+ case 'WorkspacesChanged':
+ handleWorkspacesChanged(event.WorkspacesChanged);
+ break;
+ case 'WorkspaceActivated':
+ handleWorkspaceActivated(event.WorkspaceActivated);
+ break;
+ case 'WorkspaceActiveWindowChanged':
+ handleWorkspaceActiveWindowChanged(event.WorkspaceActiveWindowChanged);
+ break;
+ case 'WindowFocusChanged':
+ handleWindowFocusChanged(event.WindowFocusChanged);
+ break;
+ case 'WindowsChanged':
+ handleWindowsChanged(event.WindowsChanged);
+ break;
+ case 'WindowClosed':
+ handleWindowClosed(event.WindowClosed);
+ break;
+ case 'WindowOpenedOrChanged':
+ handleWindowOpenedOrChanged(event.WindowOpenedOrChanged);
+ break;
+ case 'WindowLayoutsChanged':
+ handleWindowLayoutsChanged(event.WindowLayoutsChanged);
+ break;
+ case 'OutputsChanged':
+ handleOutputsChanged(event.OutputsChanged);
+ break;
+ case 'OverviewOpenedOrClosed':
+ handleOverviewChanged(event.OverviewOpenedOrClosed);
+ break;
+ case 'ConfigLoaded':
+ handleConfigLoaded(event.ConfigLoaded);
+ break;
+ case 'KeyboardLayoutsChanged':
+ handleKeyboardLayoutsChanged(event.KeyboardLayoutsChanged);
+ break;
+ case 'KeyboardLayoutSwitched':
+ handleKeyboardLayoutSwitched(event.KeyboardLayoutSwitched);
+ break;
+ case 'WorkspaceUrgencyChanged':
+ handleWorkspaceUrgencyChanged(event.WorkspaceUrgencyChanged);
+ break;
+ case 'WindowUrgencyChanged':
+ handleWindowUrgencyChanged(event.WindowUrgencyChanged);
+ break;
+ case 'ScreenshotCaptured':
+ handleScreenshotCaptured(event.ScreenshotCaptured);
+ break;
+ case 'CastsChanged':
+ handleCastsChanged(event.CastsChanged);
+ break;
+ case 'CastStartedOrChanged':
+ handleCastStartedOrChanged(event.CastStartedOrChanged);
+ break;
+ case 'CastStopped':
+ handleCastStopped(event.CastStopped);
+ break;
+ }
+ }
+
+ function handleWorkspacesChanged(data) {
+ const newWorkspaces = {};
+
+ for (const ws of data.workspaces) {
+ const oldWs = root.workspaces[ws.id];
+ newWorkspaces[ws.id] = ws;
+ if (oldWs && oldWs.active_window_id !== undefined) {
+ newWorkspaces[ws.id].active_window_id = oldWs.active_window_id;
+ }
+ }
+
+ setWorkspaces(newWorkspaces);
+
+ focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.is_focused);
+ if (focusedWorkspaceIndex >= 0) {
+ const focusedWs = allWorkspaces[focusedWorkspaceIndex];
+ focusedWorkspaceId = focusedWs.id;
+ currentOutput = focusedWs.output || "";
+ } else {
+ focusedWorkspaceIndex = 0;
+ focusedWorkspaceId = "";
+ }
+
+ updateCurrentOutputWorkspaces();
+ }
+
+ function handleWorkspaceActivated(data) {
+ const ws = root.workspaces[data.id];
+ if (!ws) {
+ return;
+ }
+ const output = ws.output;
+
+ const updatedWorkspaces = {};
+
+ for (const id in root.workspaces) {
+ const workspace = root.workspaces[id];
+ const got_activated = workspace.id === data.id;
+
+ const updatedWs = {};
+ for (let prop in workspace) {
+ updatedWs[prop] = workspace[prop];
+ }
+
+ if (workspace.output === output) {
+ updatedWs.is_active = got_activated;
+ }
+
+ if (data.focused) {
+ updatedWs.is_focused = got_activated;
+ }
+
+ updatedWorkspaces[id] = updatedWs;
+ }
+
+ setWorkspaces(updatedWorkspaces);
+
+ focusedWorkspaceId = data.id;
+ focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.id === data.id);
+
+ if (focusedWorkspaceIndex >= 0) {
+ currentOutput = allWorkspaces[focusedWorkspaceIndex].output || "";
+ }
+
+ updateCurrentOutputWorkspaces();
+ }
+
+ function handleWindowFocusChanged(data) {
+ const focusedWindowId = data.id;
+
+ let focusedWindow = null;
+ const updatedWindows = [];
+
+ for (var i = 0; i < windows.length; i++) {
+ const w = windows[i];
+ const updatedWindow = {};
+
+ for (let prop in w) {
+ updatedWindow[prop] = w[prop];
+ }
+
+ updatedWindow.is_focused = (w.id === focusedWindowId);
+ if (updatedWindow.is_focused) {
+ focusedWindow = updatedWindow;
+ }
+
+ updatedWindows.push(updatedWindow);
+ }
+
+ windows = updatedWindows;
+
+ if (focusedWindow) {
+ const ws = root.workspaces[focusedWindow.workspace_id];
+ if (ws && ws.active_window_id !== focusedWindowId) {
+ const updatedWs = {};
+ for (let prop in ws) {
+ updatedWs[prop] = ws[prop];
+ }
+ updatedWs.active_window_id = focusedWindowId;
+
+ const updatedWorkspaces = {};
+ for (const id in root.workspaces) {
+ updatedWorkspaces[id] = id === focusedWindow.workspace_id ? updatedWs : root.workspaces[id];
+ }
+ setWorkspaces(updatedWorkspaces);
+ }
+ }
+ }
+
+ function handleWorkspaceActiveWindowChanged(data) {
+ const ws = root.workspaces[data.workspace_id];
+ if (ws) {
+ const updatedWs = {};
+ for (let prop in ws) {
+ updatedWs[prop] = ws[prop];
+ }
+ updatedWs.active_window_id = data.active_window_id;
+
+ const updatedWorkspaces = {};
+ for (const id in root.workspaces) {
+ updatedWorkspaces[id] = id === data.workspace_id ? updatedWs : root.workspaces[id];
+ }
+ setWorkspaces(updatedWorkspaces);
+ }
+
+ const updatedWindows = [];
+
+ for (var i = 0; i < windows.length; i++) {
+ const w = windows[i];
+ const updatedWindow = {};
+
+ for (let prop in w) {
+ updatedWindow[prop] = w[prop];
+ }
+
+ if (data.active_window_id !== null && data.active_window_id !== undefined) {
+ updatedWindow.is_focused = (w.id == data.active_window_id);
+ } else {
+ updatedWindow.is_focused = w.workspace_id == data.workspace_id ? false : w.is_focused;
+ }
+
+ updatedWindows.push(updatedWindow);
+ }
+
+ windows = updatedWindows;
+ }
+
+ function handleWindowsChanged(data) {
+ windows = sortWindowsByLayout(data.windows);
+ }
+
+ function handleWindowClosed(data) {
+ windows = windows.filter(w => w.id !== data.id);
+ }
+
+ function handleWindowOpenedOrChanged(data) {
+ if (!data.window)
+ return;
+ const window = data.window;
+ const existingIndex = windows.findIndex(w => w.id === window.id);
+
+ if (existingIndex >= 0) {
+ const updatedWindows = [...windows];
+ updatedWindows[existingIndex] = window;
+ windows = sortWindowsByLayout(updatedWindows);
+ } else {
+ windows = sortWindowsByLayout([...windows, window]);
+ }
+ }
+
+ function handleWindowLayoutsChanged(data) {
+ if (!data.changes)
+ return;
+ const updatedWindows = [...windows];
+ let hasChanges = false;
+
+ for (const change of data.changes) {
+ const windowId = change[0];
+ const layoutData = change[1];
+
+ const windowIndex = updatedWindows.findIndex(w => w.id === windowId);
+ if (windowIndex < 0)
+ continue;
+ const updatedWindow = {};
+ for (var prop in updatedWindows[windowIndex]) {
+ updatedWindow[prop] = updatedWindows[windowIndex][prop];
+ }
+ updatedWindow.layout = layoutData;
+ updatedWindows[windowIndex] = updatedWindow;
+ hasChanges = true;
+ }
+
+ if (!hasChanges)
+ return;
+ windows = sortWindowsByLayout(updatedWindows);
+ }
+
+ function handleOutputsChanged(data) {
+ if (!data.outputs)
+ return;
+ outputs = data.outputs;
+ updateDisplayScales();
+ windows = sortWindowsByLayout(windows);
+ }
+
+ function handleOverviewChanged(data) {
+ inOverview = data.is_open;
+ }
+
+ function handleConfigLoaded(data) {
+ if (data.failed) {
+ validateProcess.running = true;
+ return;
+ }
+
+ configValidationOutput = "";
+ ToastService.dismissCategory("niri-config");
+ fetchOutputs();
+ configReloaded();
+
+ if (hasInitialConnection && !suppressConfigToast && !suppressNextConfigToast && !matugenSuppression) {
+ ToastService.showInfo("niri: config reloaded", "", "", "niri-config");
+ } else if (suppressNextConfigToast) {
+ suppressNextConfigToast = false;
+ suppressResetTimer.stop();
+ }
+
+ if (!hasInitialConnection) {
+ hasInitialConnection = true;
+ suppressToastTimer.start();
+ }
+ }
+
+ function handleKeyboardLayoutsChanged(data) {
+ keyboardLayoutNames = data.keyboard_layouts.names;
+ currentKeyboardLayoutIndex = data.keyboard_layouts.current_idx;
+ }
+
+ function handleKeyboardLayoutSwitched(data) {
+ currentKeyboardLayoutIndex = data.idx;
+ }
+
+ function handleWorkspaceUrgencyChanged(data) {
+ const ws = root.workspaces[data.id];
+ if (!ws)
+ return;
+ const updatedWs = {};
+ for (let prop in ws) {
+ updatedWs[prop] = ws[prop];
+ }
+ updatedWs.is_urgent = data.urgent;
+
+ const updatedWorkspaces = {};
+ for (const id in root.workspaces) {
+ updatedWorkspaces[id] = id === data.id ? updatedWs : root.workspaces[id];
+ }
+ setWorkspaces(updatedWorkspaces);
+
+ windowUrgentChanged();
+ }
+
+ function handleWindowUrgencyChanged(data) {
+ const windowIndex = windows.findIndex(w => w.id === data.id);
+ if (windowIndex < 0)
+ return;
+ const updatedWindows = [...windows];
+ const updatedWindow = {};
+ for (let prop in updatedWindows[windowIndex]) {
+ updatedWindow[prop] = updatedWindows[windowIndex][prop];
+ }
+ updatedWindow.is_urgent = data.urgent;
+ updatedWindows[windowIndex] = updatedWindow;
+ windows = updatedWindows;
+
+ windowUrgentChanged();
+ }
+
+ function handleScreenshotCaptured(data) {
+ if (!data.path)
+ return;
+ if (pendingScreenshotPath && data.path === pendingScreenshotPath) {
+ const editor = Quickshell.env("DMS_SCREENSHOT_EDITOR");
+ let command;
+ if (editor === "satty" || !editor) {
+ command = ["satty", "-f", data.path];
+ } else if (editor === "swappy") {
+ command = ["swappy", "-f", data.path];
+ } else {
+ // Custom command with %path% placeholder
+ command = editor.split(" ").map(arg => arg === "%path%" ? data.path : arg);
+ }
+ Quickshell.execDetached({
+ "command": command
+ });
+ pendingScreenshotPath = "";
+ }
+ }
+
+ function handleCastsChanged(data) {
+ casts = data.casts || [];
+ }
+
+ function handleCastStartedOrChanged(data) {
+ if (!data.cast)
+ return;
+ const cast = data.cast;
+ const existingIndex = casts.findIndex(c => c.stream_id === cast.stream_id);
+ if (existingIndex >= 0) {
+ const updatedCasts = [...casts];
+ updatedCasts[existingIndex] = cast;
+ casts = updatedCasts;
+ } else {
+ casts = [...casts, cast];
+ }
+ }
+
+ function handleCastStopped(data) {
+ casts = casts.filter(c => c.stream_id !== data.stream_id);
+ }
+
+ function updateCurrentOutputWorkspaces() {
+ if (!currentOutput) {
+ currentOutputWorkspaces = allWorkspaces;
+ return;
+ }
+
+ const outputWs = allWorkspaces.filter(w => w.output === currentOutput);
+ currentOutputWorkspaces = outputWs;
+ }
+
+ function send(request) {
+ if (!CompositorService.isNiri || !requestSocket.connected)
+ return false;
+ requestSocket.send(request);
+ return true;
+ }
+
+ function doScreenTransition() {
+ return send({
+ "Action": {
+ "DoScreenTransition": {
+ "delay_ms": 0
+ }
+ }
+ });
+ }
+
+ function toggleOverview() {
+ return send({
+ "Action": {
+ "ToggleOverview": {}
+ }
+ });
+ }
+
+ function moveColumnLeft() {
+ return send({
+ "Action": {
+ "FocusColumnLeft": {}
+ }
+ });
+ }
+
+ function moveColumnRight() {
+ return send({
+ "Action": {
+ "FocusColumnRight": {}
+ }
+ });
+ }
+
+ function moveWorkspaceDown() {
+ return send({
+ "Action": {
+ "FocusWorkspaceDown": {}
+ }
+ });
+ }
+
+ function moveWorkspaceUp() {
+ return send({
+ "Action": {
+ "FocusWorkspaceUp": {}
+ }
+ });
+ }
+
+ function switchToWorkspace(workspaceIndex) {
+ return send({
+ "Action": {
+ "FocusWorkspace": {
+ "reference": {
+ "Index": workspaceIndex
+ }
+ }
+ }
+ });
+ }
+
+ function focusWindow(windowId) {
+ return send({
+ "Action": {
+ "FocusWindow": {
+ "id": windowId
+ }
+ }
+ });
+ }
+
+ function powerOffMonitors() {
+ return send({
+ "Action": {
+ "PowerOffMonitors": {}
+ }
+ });
+ }
+
+ function powerOnMonitors() {
+ return send({
+ "Action": {
+ "PowerOnMonitors": {}
+ }
+ });
+ }
+
+ function cycleKeyboardLayout() {
+ return send({
+ "Action": {
+ "SwitchLayout": {
+ "layout": "Next"
+ }
+ }
+ });
+ }
+
+ function quit() {
+ return send({
+ "Action": {
+ "Quit": {
+ "skip_confirmation": true
+ }
+ }
+ });
+ }
+
+ function screenshot() {
+ pendingScreenshotPath = "";
+ const timestamp = Date.now();
+ const path = `${screenshotsDir}/dms-screenshot-${timestamp}.png`;
+ pendingScreenshotPath = path;
+
+ return send({
+ "Action": {
+ "Screenshot": {
+ "show_pointer": true,
+ "path": path
+ }
+ }
+ });
+ }
+
+ function screenshotScreen() {
+ pendingScreenshotPath = "";
+ const timestamp = Date.now();
+ const path = `${screenshotsDir}/dms-screenshot-${timestamp}.png`;
+ pendingScreenshotPath = path;
+
+ return send({
+ "Action": {
+ "ScreenshotScreen": {
+ "write_to_disk": true,
+ "show_pointer": true,
+ "path": path
+ }
+ }
+ });
+ }
+
+ function screenshotWindow() {
+ pendingScreenshotPath = "";
+ const timestamp = Date.now();
+ const path = `${screenshotsDir}/dms-screenshot-${timestamp}.png`;
+ pendingScreenshotPath = path;
+
+ return send({
+ "Action": {
+ "ScreenshotWindow": {
+ "write_to_disk": true,
+ "show_pointer": true,
+ "path": path
+ }
+ }
+ });
+ }
+
+ function getCurrentOutputWorkspaceNumbers() {
+ return currentOutputWorkspaces.map(w => w.idx + 1);
+ }
+
+ function getCurrentOutputWorkspaces() {
+ return currentOutputWorkspaces.slice();
+ }
+
+ function getCurrentWorkspaceNumber() {
+ if (focusedWorkspaceIndex >= 0 && focusedWorkspaceIndex < allWorkspaces.length) {
+ return allWorkspaces[focusedWorkspaceIndex].idx;
+ }
+ return 1;
+ }
+
+ function getCurrentKeyboardLayoutName() {
+ if (currentKeyboardLayoutIndex >= 0 && currentKeyboardLayoutIndex < keyboardLayoutNames.length) {
+ return keyboardLayoutNames[currentKeyboardLayoutIndex];
+ }
+ return "";
+ }
+
+ function suppressNextToast() {
+ matugenSuppression = true;
+ suppressResetTimer.restart();
+ }
+
+ function findNiriWindow(toplevel) {
+ if (!toplevel.appId)
+ return null;
+
+ for (var j = 0; j < windows.length; j++) {
+ const niriWindow = windows[j];
+ if (niriWindow.app_id === toplevel.appId) {
+ if (!niriWindow.title || niriWindow.title === toplevel.title) {
+ return {
+ "niriIndex": j,
+ "niriWindow": niriWindow
+ };
+ }
+ }
+ }
+ return null;
+ }
+
+ function sortToplevels(toplevels) {
+ if (!toplevels || toplevels.length === 0 || !CompositorService.isNiri || windows.length === 0) {
+ return [...toplevels];
+ }
+
+ const usedToplevels = new Set();
+ const enrichedToplevels = [];
+
+ for (const niriWindow of sortWindowsByLayout(windows)) {
+ let bestMatch = null;
+ let bestScore = -1;
+
+ for (const toplevel of toplevels) {
+ if (usedToplevels.has(toplevel))
+ continue;
+ if (toplevel.appId === niriWindow.app_id) {
+ let score = 1;
+
+ if (niriWindow.title && toplevel.title) {
+ if (toplevel.title === niriWindow.title) {
+ score = 3;
+ } else if (toplevel.title.includes(niriWindow.title) || niriWindow.title.includes(toplevel.title)) {
+ score = 2;
+ }
+ }
+
+ if (score > bestScore) {
+ bestScore = score;
+ bestMatch = toplevel;
+ if (score === 3)
+ break;
+ }
+ }
+ }
+
+ if (!bestMatch)
+ continue;
+ usedToplevels.add(bestMatch);
+
+ const workspace = workspaces[niriWindow.workspace_id];
+ const isFocused = niriWindow.is_focused ?? (workspace && workspace.active_window_id === niriWindow.id) ?? false;
+
+ const enrichedToplevel = {
+ "appId": bestMatch.appId,
+ "title": bestMatch.title,
+ "activated": isFocused,
+ "niriWindowId": niriWindow.id,
+ "niriWorkspaceId": niriWindow.workspace_id,
+ "activate": function () {
+ return NiriService.focusWindow(niriWindow.id);
+ },
+ "close": function () {
+ if (bestMatch.close) {
+ return bestMatch.close();
+ }
+ return false;
+ }
+ };
+
+ for (let prop in bestMatch) {
+ if (!(prop in enrichedToplevel)) {
+ enrichedToplevel[prop] = bestMatch[prop];
+ }
+ }
+
+ enrichedToplevels.push(enrichedToplevel);
+ }
+
+ for (const toplevel of toplevels) {
+ if (!usedToplevels.has(toplevel)) {
+ enrichedToplevels.push(toplevel);
+ }
+ }
+
+ return enrichedToplevels;
+ }
+
+ function _matchAndEnrichToplevels(toplevels, niriWindows) {
+ const usedToplevels = new Set();
+ const result = [];
+
+ for (const niriWindow of niriWindows) {
+ let bestMatch = null;
+ let bestScore = -1;
+
+ for (const toplevel of toplevels) {
+ if (usedToplevels.has(toplevel))
+ continue;
+ if (toplevel.appId !== niriWindow.app_id)
+ continue;
+
+ let score = 1;
+ if (niriWindow.title && toplevel.title) {
+ if (toplevel.title === niriWindow.title) {
+ score = 3;
+ } else if (toplevel.title.includes(niriWindow.title) || niriWindow.title.includes(toplevel.title)) {
+ score = 2;
+ }
+ }
+
+ if (score > bestScore) {
+ bestScore = score;
+ bestMatch = toplevel;
+ if (score === 3)
+ break;
+ }
+ }
+
+ if (!bestMatch)
+ continue;
+ usedToplevels.add(bestMatch);
+
+ const workspace = workspaces[niriWindow.workspace_id];
+ const isFocused = niriWindow.is_focused ?? (workspace && workspace.active_window_id === niriWindow.id) ?? false;
+
+ const enrichedToplevel = {
+ "appId": bestMatch.appId,
+ "title": bestMatch.title,
+ "activated": isFocused,
+ "niriWindowId": niriWindow.id,
+ "niriWorkspaceId": niriWindow.workspace_id,
+ "activate": function () {
+ return NiriService.focusWindow(niriWindow.id);
+ },
+ "close": function () {
+ if (bestMatch.close)
+ return bestMatch.close();
+ return false;
+ }
+ };
+
+ for (let prop in bestMatch) {
+ if (!(prop in enrichedToplevel))
+ enrichedToplevel[prop] = bestMatch[prop];
+ }
+
+ result.push(enrichedToplevel);
+ }
+
+ return result;
+ }
+
+ function filterCurrentWorkspace(toplevels, screenName) {
+ let currentWorkspaceId = null;
+
+ for (var i = 0; i < allWorkspaces.length; i++) {
+ const ws = allWorkspaces[i];
+ if (ws.output === screenName && ws.is_active) {
+ currentWorkspaceId = ws.id;
+ break;
+ }
+ }
+
+ if (currentWorkspaceId === null)
+ return toplevels;
+
+ if (toplevels.length > 0 && toplevels[0].niriWorkspaceId !== undefined)
+ return toplevels.filter(t => t.niriWorkspaceId === currentWorkspaceId);
+
+ return _matchAndEnrichToplevels(toplevels, windows.filter(nw => nw.workspace_id === currentWorkspaceId));
+ }
+
+ function filterCurrentDisplay(toplevels, screenName) {
+ if (!toplevels || toplevels.length === 0 || !screenName)
+ return toplevels;
+
+ const outputWorkspaceIds = new Set();
+ for (var i = 0; i < allWorkspaces.length; i++) {
+ const ws = allWorkspaces[i];
+ if (ws.output === screenName)
+ outputWorkspaceIds.add(ws.id);
+ }
+
+ if (outputWorkspaceIds.size === 0)
+ return toplevels;
+
+ if (toplevels.length > 0 && toplevels[0].niriWorkspaceId !== undefined)
+ return toplevels.filter(t => outputWorkspaceIds.has(t.niriWorkspaceId));
+
+ return _matchAndEnrichToplevels(toplevels, windows.filter(nw => outputWorkspaceIds.has(nw.workspace_id)));
+ }
+
+ function generateNiriLayoutConfig() {
+ if (!CompositorService.isNiri || configGenerationPending)
+ return;
+ suppressNextToast();
+ configGenerationPending = true;
+ configGenerationDebounce.restart();
+ }
+
+ function doGenerateNiriLayoutConfig() {
+ console.log("NiriService: Generating layout config...");
+
+ const defaultRadius = typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12;
+ const defaultGaps = typeof SettingsData !== "undefined" ? Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)) : 4;
+ const defaultBorderSize = 2;
+
+ const cornerRadius = (typeof SettingsData !== "undefined" && SettingsData.niriLayoutRadiusOverride >= 0) ? SettingsData.niriLayoutRadiusOverride : defaultRadius;
+ const gaps = (typeof SettingsData !== "undefined" && SettingsData.niriLayoutGapsOverride >= 0) ? SettingsData.niriLayoutGapsOverride : defaultGaps;
+ const borderSize = (typeof SettingsData !== "undefined" && SettingsData.niriLayoutBorderSize >= 0) ? SettingsData.niriLayoutBorderSize : defaultBorderSize;
+
+ const dmsWarning = `// ! DO NOT EDIT !
+ // ! AUTO-GENERATED BY DMS !
+ // ! CHANGES WILL BE OVERWRITTEN !
+ // ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
+
+ `;
+
+ const configContent = dmsWarning + `layout {
+ gaps ${gaps}
+
+ border {
+ width ${borderSize}
+ }
+
+ focus-ring {
+ width ${borderSize}
+ }
+ }
+ window-rule {
+ geometry-corner-radius ${cornerRadius}
+ clip-to-geometry true
+ tiled-state true
+ draw-border-with-background false
+ }`;
+
+ const alttabContent = dmsWarning + `recent-windows {
+ highlight {
+ corner-radius ${cornerRadius}
+ }
+ }`;
+
+ const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation));
+ const niriDmsDir = configDir + "/niri/dms";
+ const configPath = niriDmsDir + "/layout.kdl";
+ const alttabPath = niriDmsDir + "/alttab.kdl";
+
+ writeConfigProcess.configContent = configContent;
+ writeConfigProcess.configPath = configPath;
+ writeConfigProcess.command = ["sh", "-c", `mkdir -p "${niriDmsDir}" && cat > "${configPath}" << 'EOF'\n${configContent}\nEOF`];
+ writeConfigProcess.running = true;
+
+ writeAlttabProcess.alttabContent = alttabContent;
+ writeAlttabProcess.alttabPath = alttabPath;
+ writeAlttabProcess.command = ["sh", "-c", `mkdir -p "${niriDmsDir}" && cat > "${alttabPath}" << 'EOF'\n${alttabContent}\nEOF`];
+ writeAlttabProcess.running = true;
+
+ for (const name of ["outputs", "binds", "cursor", "windowrules", "colors", "alttab", "layout"]) {
+ const path = niriDmsDir + "/" + name + ".kdl";
+ Proc.runCommand("niri-ensure-" + name, ["sh", "-c", `mkdir -p "${niriDmsDir}" && [ ! -f "${path}" ] && touch "${path}" || true`], (output, exitCode) => {
+ if (exitCode !== 0)
+ console.warn("NiriService: Failed to ensure " + name + ".kdl, exit code:", exitCode);
+ });
+ }
+
+ configGenerationPending = false;
+ }
+
+ function generateNiriBlurrule() {
+ console.log("NiriService: Generating wpblur config...");
+
+ const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation));
+ const niriDmsDir = configDir + "/niri/dms";
+ const blurrulePath = niriDmsDir + "/wpblur.kdl";
+ const sourceBlurrulePath = Paths.strip(Qt.resolvedUrl("niri-wpblur.kdl"));
+
+ writeBlurruleProcess.blurrulePath = blurrulePath;
+ writeBlurruleProcess.command = ["sh", "-c", `mkdir -p "${niriDmsDir}" && cp --no-preserve=mode "${sourceBlurrulePath}" "${blurrulePath}"`];
+ writeBlurruleProcess.running = true;
+ }
+
+ function generateNiriCursorConfig() {
+ if (!CompositorService.isNiri)
+ return;
+
+ console.log("NiriService: Generating cursor config...");
+
+ const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation));
+ const niriDmsDir = configDir + "/niri/dms";
+ const cursorPath = niriDmsDir + "/cursor.kdl";
+
+ const settings = typeof SettingsData !== "undefined" ? SettingsData.cursorSettings : null;
+ if (!settings) {
+ writeCursorProcess.cursorContent = "";
+ writeCursorProcess.cursorPath = cursorPath;
+ writeCursorProcess.command = ["sh", "-c", `mkdir -p "${niriDmsDir}" && : > "${cursorPath}"`];
+ writeCursorProcess.running = true;
+ return;
+ }
+
+ const themeName = settings.theme === "System Default" ? (SettingsData.systemDefaultCursorTheme || "") : settings.theme;
+ const size = settings.size || 24;
+ const hideWhenTyping = settings.niri?.hideWhenTyping || false;
+ const hideAfterMs = settings.niri?.hideAfterInactiveMs || 0;
+
+ const isDefaultConfig = !themeName && size === 24 && !hideWhenTyping && hideAfterMs === 0;
+ if (isDefaultConfig) {
+ writeCursorProcess.cursorContent = "";
+ writeCursorProcess.cursorPath = cursorPath;
+ writeCursorProcess.command = ["sh", "-c", `mkdir -p "${niriDmsDir}" && : > "${cursorPath}"`];
+ writeCursorProcess.running = true;
+ return;
+ }
+
+ const dmsWarning = `// ! DO NOT EDIT !
+// ! AUTO-GENERATED BY DMS !
+// ! CHANGES WILL BE OVERWRITTEN !
+// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
+
+`;
+
+ let cursorContent = dmsWarning + `cursor {\n`;
+
+ if (themeName)
+ cursorContent += ` xcursor-theme "${themeName}"\n`;
+
+ cursorContent += ` xcursor-size ${size}\n`;
+
+ if (hideWhenTyping)
+ cursorContent += ` hide-when-typing\n`;
+
+ if (hideAfterMs > 0)
+ cursorContent += ` hide-after-inactive-ms ${hideAfterMs}\n`;
+
+ cursorContent += `}`;
+
+ writeCursorProcess.cursorContent = cursorContent;
+ writeCursorProcess.cursorPath = cursorPath;
+
+ const escapedCursorContent = cursorContent.replace(/'/g, "'\\''");
+
+ writeCursorProcess.command = ["sh", "-c", `mkdir -p "${niriDmsDir}" && printf '%s' '${escapedCursorContent}' > "${cursorPath}"`];
+ writeCursorProcess.running = true;
+ }
+
+ function updateOutputPosition(outputName, x, y) {
+ if (!outputs || !outputs[outputName])
+ return;
+ const updatedOutputs = {};
+ for (const name in outputs) {
+ const output = outputs[name];
+ if (name === outputName && output.logical) {
+ updatedOutputs[name] = JSON.parse(JSON.stringify(output));
+ updatedOutputs[name].logical.x = x;
+ updatedOutputs[name].logical.y = y;
+ } else {
+ updatedOutputs[name] = output;
+ }
+ }
+ outputs = updatedOutputs;
+ }
+
+ function applyOutputConfig(outputName, config, callback) {
+ if (!CompositorService.isNiri || !outputName) {
+ if (callback)
+ callback(false, "Invalid config");
+ return;
+ }
+
+ const commands = [];
+
+ if (config.position !== undefined) {
+ commands.push(`niri msg output "${outputName}" position ${config.position.x} ${config.position.y}`);
+ }
+
+ if (config.mode !== undefined) {
+ commands.push(`niri msg output "${outputName}" mode ${config.mode}`);
+ }
+
+ if (config.vrr !== undefined) {
+ commands.push(`niri msg output "${outputName}" vrr ${config.vrr ? "on" : "off"}`);
+ }
+
+ if (config.scale !== undefined) {
+ commands.push(`niri msg output "${outputName}" scale ${config.scale}`);
+ }
+
+ if (config.transform !== undefined) {
+ commands.push(`niri msg output "${outputName}" transform "${config.transform}"`);
+ }
+
+ if (commands.length === 0) {
+ if (callback)
+ callback(true, "No changes");
+ return;
+ }
+
+ const fullCommand = commands.join(" && ");
+ Proc.runCommand("niri-output-config", ["sh", "-c", fullCommand], (output, exitCode) => {
+ if (exitCode !== 0) {
+ console.warn("NiriService: Failed to apply output config:", output);
+ if (callback)
+ callback(false, output);
+ return;
+ }
+ console.info("NiriService: Applied output config for", outputName);
+ fetchOutputs();
+ if (callback)
+ callback(true, "Success");
+ });
+ }
+
+ function getOutputIdentifier(output, outputName) {
+ if (SettingsData.displayNameMode === "model" && output.make && output.model) {
+ const serial = output.serial || "Unknown";
+ return output.make + " " + output.model + " " + serial;
+ }
+ return outputName;
+ }
+
+ function generateOutputsConfig(outputsData) {
+ const data = outputsData || outputs;
+ if (!data || Object.keys(data).length === 0)
+ return;
+ let kdlContent = `// Auto-generated by DMS - do not edit manually\n\n`;
+
+ const sortedNames = Object.keys(data).sort((a, b) => {
+ const la = data[a].logical || {};
+ const lb = data[b].logical || {};
+ return (la.x ?? 0) - (lb.x ?? 0) || (la.y ?? 0) - (lb.y ?? 0);
+ });
+ for (const outputName of sortedNames) {
+ const output = data[outputName];
+ const identifier = getOutputIdentifier(output, outputName);
+ const niriSettings = SettingsData.getNiriOutputSettings(identifier);
+
+ kdlContent += `output "${identifier}" {\n`;
+
+ if (niriSettings.disabled) {
+ kdlContent += ` off\n`;
+ }
+
+ if (output.current_mode !== undefined && output.modes && output.modes[output.current_mode]) {
+ const mode = output.modes[output.current_mode];
+ kdlContent += ` mode "${mode.width}x${mode.height}@${(mode.refresh_rate / 1000).toFixed(3)}"\n`;
+ }
+
+ if (output.logical) {
+ kdlContent += ` scale ${output.logical.scale || 1.0}\n`;
+
+ if (output.logical.transform && output.logical.transform !== "Normal") {
+ const transformMap = {
+ "Normal": "normal",
+ "90": "90",
+ "180": "180",
+ "270": "270",
+ "Flipped": "flipped",
+ "Flipped90": "flipped-90",
+ "Flipped180": "flipped-180",
+ "Flipped270": "flipped-270"
+ };
+ kdlContent += ` transform "${transformMap[output.logical.transform] || "normal"}"\n`;
+ }
+
+ if (output.logical.x !== undefined && output.logical.y !== undefined) {
+ kdlContent += ` position x=${output.logical.x} y=${output.logical.y}\n`;
+ }
+ }
+
+ if (output.vrr_enabled || niriSettings.vrrOnDemand) {
+ const vrrOnDemand = niriSettings.vrrOnDemand ?? false;
+ kdlContent += vrrOnDemand ? ` variable-refresh-rate on-demand=true\n` : ` variable-refresh-rate\n`;
+ }
+
+ if (niriSettings.focusAtStartup) {
+ kdlContent += ` focus-at-startup\n`;
+ }
+
+ if (niriSettings.backdropColor) {
+ kdlContent += ` backdrop-color "${niriSettings.backdropColor}"\n`;
+ }
+
+ kdlContent += generateHotCornersBlock(niriSettings);
+ kdlContent += generateLayoutBlock(niriSettings);
+
+ kdlContent += `}\n\n`;
+ }
+
+ const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation));
+ const niriDmsDir = configDir + "/niri/dms";
+ const outputsPath = niriDmsDir + "/outputs.kdl";
+
+ Proc.runCommand("niri-write-outputs", ["sh", "-c", `mkdir -p "${niriDmsDir}" && cat > "${outputsPath}" << 'EOF'\n${kdlContent}EOF`], (output, exitCode) => {
+ if (exitCode !== 0) {
+ console.warn("NiriService: Failed to write outputs config:", output);
+ return;
+ }
+ console.info("NiriService: Generated outputs config at", outputsPath);
+ });
+ }
+
+ function generateHotCornersBlock(niriSettings) {
+ if (!niriSettings.hotCorners)
+ return "";
+ const hc = niriSettings.hotCorners;
+ if (hc.off)
+ return ` hot-corners {\n off\n }\n`;
+ const corners = hc.corners || [];
+ if (corners.length === 0)
+ return "";
+ let block = ` hot-corners {\n`;
+ for (const corner of corners) {
+ block += ` ${corner}\n`;
+ }
+ block += ` }\n`;
+ return block;
+ }
+
+ function generateLayoutBlock(niriSettings) {
+ if (!niriSettings.layout)
+ return "";
+ const layout = niriSettings.layout;
+ const hasSettings = layout.gaps !== undefined || layout.defaultColumnWidth || layout.presetColumnWidths || layout.alwaysCenterSingleColumn !== undefined;
+ if (!hasSettings)
+ return "";
+ let block = ` layout {\n`;
+ if (layout.gaps !== undefined)
+ block += ` gaps ${layout.gaps}\n`;
+ if (layout.defaultColumnWidth?.type === "proportion") {
+ const val = layout.defaultColumnWidth.value;
+ const formatted = Number.isInteger(val) ? val.toFixed(1) : val.toString();
+ block += ` default-column-width { proportion ${formatted}; }\n`;
+ }
+ if (layout.presetColumnWidths && layout.presetColumnWidths.length > 0) {
+ block += ` preset-column-widths {\n`;
+ for (const preset of layout.presetColumnWidths) {
+ if (preset.type === "proportion") {
+ const val = preset.value;
+ const formatted = Number.isInteger(val) ? val.toFixed(1) : val.toString();
+ block += ` proportion ${formatted}\n`;
+ }
+ }
+ block += ` }\n`;
+ }
+ if (layout.alwaysCenterSingleColumn !== undefined)
+ block += layout.alwaysCenterSingleColumn ? ` always-center-single-column\n` : ` always-center-single-column false\n`;
+ block += ` }\n`;
+ return block;
+ }
+
+ function renameWorkspace(name) {
+ if (!name || name.trim() === "") {
+ return send({
+ "Action": {
+ "UnsetWorkspaceName": {
+ "workspace": null
+ }
+ }
+ });
+ }
+ return send({
+ "Action": {
+ "SetWorkspaceName": {
+ "name": name,
+ "workspace": null
+ }
+ }
+ });
+ }
+
+ function moveWorkspaceToIndex(workspaceIdx, targetIndex) {
+ return send({
+ "Action": {
+ "MoveWorkspaceToIndex": {
+ "index": targetIndex,
+ "reference": {
+ "Index": workspaceIdx
+ }
+ }
+ }
+ });
+ }
+
+ IpcHandler {
+ function screenshot(): string {
+ if (!CompositorService.isNiri) {
+ return "NIRI_NOT_AVAILABLE";
+ }
+ if (NiriService.screenshot()) {
+ return "SCREENSHOT_SUCCESS";
+ }
+ return "SCREENSHOT_FAILED";
+ }
+
+ function screenshotScreen(): string {
+ if (!CompositorService.isNiri) {
+ return "NIRI_NOT_AVAILABLE";
+ }
+ if (NiriService.screenshotScreen()) {
+ return "SCREENSHOT_SCREEN_SUCCESS";
+ }
+ return "SCREENSHOT_SCREEN_FAILED";
+ }
+
+ function screenshotWindow(): string {
+ if (!CompositorService.isNiri) {
+ return "NIRI_NOT_AVAILABLE";
+ }
+ if (NiriService.screenshotWindow()) {
+ return "SCREENSHOT_WINDOW_SUCCESS";
+ }
+ return "SCREENSHOT_WINDOW_FAILED";
+ }
+
+ target: "niri"
+ }
+}