mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 13:32:50 -05:00
1470 lines
47 KiB
QML
1470 lines
47 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);
|
|
}
|
|
|
|
Component.onCompleted: fetchOutputs()
|
|
|
|
Timer {
|
|
id: suppressToastTimer
|
|
interval: 3000
|
|
onTriggered: root.suppressConfigToast = false
|
|
}
|
|
|
|
Timer {
|
|
id: suppressResetTimer
|
|
interval: 2000
|
|
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();
|
|
if (hasInitialConnection) {
|
|
ToastService.showError("niri: failed to load config", configValidationOutput, "", "niri-config");
|
|
}
|
|
}
|
|
}
|
|
|
|
onExited: exitCode => {
|
|
if (exitCode === 0) {
|
|
configValidationOutput = "";
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: ensureOutputsProcess
|
|
property string outputsPath: ""
|
|
|
|
onExited: exitCode => {
|
|
if (exitCode !== 0)
|
|
console.warn("NiriService: Failed to ensure outputs.kdl, exit code:", exitCode);
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: ensureBindsProcess
|
|
property string bindsPath: ""
|
|
|
|
onExited: exitCode => {
|
|
if (exitCode !== 0)
|
|
console.warn("NiriService: Failed to ensure binds.kdl, exit code:", exitCode);
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: ensureCursorProcess
|
|
property string cursorPath: ""
|
|
|
|
onExited: exitCode => {
|
|
if (exitCode !== 0)
|
|
console.warn("NiriService: Failed to ensure cursor.kdl, 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");
|
|
const command = editor === "satty" ? ["satty", "-f", data.path] : ["swappy", "-f", data.path];
|
|
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 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;
|
|
|
|
const workspaceWindows = windows.filter(niriWindow => niriWindow.workspace_id === currentWorkspaceId);
|
|
const usedToplevels = new Set();
|
|
const result = [];
|
|
|
|
for (const niriWindow of workspaceWindows) {
|
|
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];
|
|
}
|
|
}
|
|
|
|
result.push(enrichedToplevel);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
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;
|
|
|
|
const outputsPath = niriDmsDir + "/outputs.kdl";
|
|
ensureOutputsProcess.outputsPath = outputsPath;
|
|
ensureOutputsProcess.command = ["sh", "-c", `mkdir -p "${niriDmsDir}" && [ ! -f "${outputsPath}" ] && touch "${outputsPath}" || true`];
|
|
ensureOutputsProcess.running = true;
|
|
|
|
const bindsPath = niriDmsDir + "/binds.kdl";
|
|
ensureBindsProcess.bindsPath = bindsPath;
|
|
ensureBindsProcess.command = ["sh", "-c", `mkdir -p "${niriDmsDir}" && [ ! -f "${bindsPath}" ] && touch "${bindsPath}" || true`];
|
|
ensureBindsProcess.running = true;
|
|
|
|
const cursorPath = niriDmsDir + "/cursor.kdl";
|
|
ensureCursorProcess.cursorPath = cursorPath;
|
|
ensureCursorProcess.command = ["sh", "-c", `mkdir -p "${niriDmsDir}" && [ ! -f "${cursorPath}" ] && touch "${cursorPath}" || true`];
|
|
ensureCursorProcess.running = true;
|
|
|
|
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`;
|
|
|
|
for (const outputName in data) {
|
|
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) {
|
|
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) {
|
|
return send({
|
|
"Action": {
|
|
"SetWorkspaceName": {
|
|
"name": name,
|
|
"workspace": null
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
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"
|
|
}
|
|
}
|