diff options
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.qml | 1494 |
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" + } +} |