mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-03 20:32:07 -04:00
* fix: use UnsetWorkspaceName for empty input in workspace rename Previously, empty input would set workspace name to empty string, causing issues with Niri's unique workspace name requirement. Now uses UnsetWorkspaceName action when input is empty. * prevent false failed to load config toast on niri validation Move error toast logic from StdioCollector.onStreamFinished to Process.onExited so it only displays when niri validate actually fails (non-zero exit code), not when stderr outputs early progress messages during config processing.
1495 lines
48 KiB
QML
1495 lines
48 KiB
QML
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"
|
|
}
|
|
}
|