repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
quickshell/nuguland/Niri.qml
Rawimport QtQuick
import Quickshell
import Quickshell.Io
import "niri.js" as NiriState
Item {
id: root
readonly property bool available: String(Quickshell.env("NIRI_SOCKET") || "").length > 0
property bool ready: false
property bool windowsInitialized: false
property bool workspacesInitialized: false
property bool streamFailed: false
property int retryDelay: 250
property string lastError: ""
property string outputError: ""
property bool outputsPending: false
property bool outputsFinished: false
signal screenshotCaptured(var path)
property var workspaces: []
readonly property var visibleWorkspaces: displayWorkspaces(workspaces)
property var windows: []
property var outputs: ({})
property var focusedWindow: null
property string overviewState: "unknown"
property string lastPickedColor: ""
property var lastWindowAction: ({})
property var actionQueue: []
readonly property var focusedWindowRecord: windowById(focusedWindow)
readonly property string focusedTitle: focusedWindowRecord ? (focusedWindowRecord.title || "") : ""
readonly property string focusedAppId: focusedWindowRecord ? (focusedWindowRecord.app_id || focusedWindowRecord.appId || "") : ""
readonly property string focusedOutputName: focusedOutputNameFromWorkspaces(workspaces)
readonly property string keybindConfigPath: {
const xdg = String(Quickshell.env("XDG_CONFIG_HOME") || "");
const home = String(Quickshell.env("HOME") || "");
return (xdg.length > 0 ? xdg : home + "/.config") + "/niri/cfg/keybinds.kdl";
}
property var workspaceShortcuts: ({})
function refresh() {
if (!available || query.running)
return;
reconnect.stop();
resetStream();
streamFailed = false;
lastError = "";
query.exec(["niri", "msg", "--json", "event-stream"]);
}
function resetStream() {
ready = false;
windowsInitialized = false;
workspacesInitialized = false;
windows = [];
workspaces = [];
focusedWindow = null;
overviewState = "unknown";
}
function streamDisconnected() {
resetStream();
if (lastError.length === 0)
lastError = "event stream disconnected";
if (!available || reconnect.running)
return;
reconnect.interval = retryDelay;
retryDelay = Math.min(retryDelay * 2, 8000);
reconnect.start();
}
function failStream(message) {
lastError = message;
ready = false;
streamFailed = true;
query.signal(9);
}
function refreshOutputs() {
if (!available)
return;
if (outputQuery.running) {
outputsPending = true;
return;
}
outputsPending = false;
outputsFinished = false;
outputQuery.exec(["niri", "msg", "--json", "outputs"]);
}
function applyOutputs(text, code, status) {
try {
const inventory = NiriState.parseOutputs(text, code, status);
if (JSON.stringify(outputs) !== JSON.stringify(inventory))
outputs = inventory;
outputError = "";
} catch (error) {
outputError = String(error);
}
}
function action(args) {
const list = Array.isArray(args) ? args : [String(args)];
actionQueue = actionQueue.concat([["niri", "msg", "action"].concat(list.map(String))]);
dispatchNextAction();
}
function dispatchNextAction() {
if (actionProc.running || actionQueue.length === 0)
return;
const command = actionQueue[0];
actionQueue = actionQueue.slice(1);
// Process.exec terminates an already-running command; preserve rapid requests.
actionProc.exec(command);
}
function screenshot(path) {
action(["screenshot", "--path", path]);
}
function screenshotScreen(path) {
action(["screenshot-screen", "--path", path, "--write-to-disk", "true"]);
}
function screenshotWindow(path) {
action(["screenshot-window", "--path", path, "--write-to-disk", "true"]);
}
function pickColor() {
lastPickedColor = "";
colorProc.exec(["niri", "msg", "pick-color"]);
}
function focusWorkspace(ref) {
action(["focus-workspace", String(ref)]);
}
function displayWorkspaces(items) {
return (items || []).filter(workspace => {
return workspace.name || workspace.is_focused || workspace.is_active || workspace.is_urgent || workspace.active_window_id !== null && workspace.active_window_id !== undefined;
});
}
function focusedOutputNameFromWorkspaces(items) {
const list = items || [];
for (let i = 0; i < list.length; i++)
if (list[i].is_focused)
return String(list[i].output || "");
return "";
}
function shortcutKey(sequence) {
const parts = String(sequence || "").split("+");
return parts.length > 0 ? parts[parts.length - 1] : "";
}
function parseWorkspaceShortcuts(text) {
const map = ({});
const re = /(?:^|\n)\s*([^\s{]+)[^{]*\{([^{}]*)\}/g;
let match;
while ((match = re.exec(String(text || ""))) !== null) {
const focus = /\bfocus-workspace\s+(?:"([^"]+)"|([^\s}]+))/.exec(match[2]);
if (!focus)
continue;
const name = String(focus[1] || focus[2] || "");
if (name.length > 0 && !map[name])
map[name] = shortcutKey(match[1]);
}
return map;
}
function reloadWorkspaceShortcuts() {
workspaceShortcuts = parseWorkspaceShortcuts(keybindFile.text());
}
function workspaceShortcut(name) {
return workspaceShortcuts[String(name || "")] || "";
}
// Group visible workspaces by output, each output's list idx-sorted, so the
// single primary bar renders every monitor's workspaces in its own tray.
// Pure: pass `items` for testing; defaults to the live workspace array.
function groupWorkspaces(items) {
const vis = displayWorkspaces(items === undefined ? workspaces : items);
const byOutput = ({});
const order = [];
for (let i = 0; i < vis.length; i++) {
const name = String(vis[i].output || "");
if (!byOutput[name]) {
byOutput[name] = [];
order.push(name);
}
byOutput[name].push(Object.assign({}, vis[i], {
"shortcut": workspaceShortcut(vis[i].name)
}));
}
return order.map(name => {
return {
"output": name,
"workspaces": byOutput[name].slice().sort((a, b) => Number(a.idx || 0) - Number(b.idx || 0))
};
});
}
// Installed niri's outputs request returns a name-keyed object.
function outputList() {
return Object.keys(outputs).map(name => outputs[name]);
}
// physical left-to-right order key for an output, from niri's logical x;
// large fallback so unknown outputs sort last (stable, not hash order).
function outputX(name) {
const list = outputList();
const want = String(name || "");
for (let i = 0; i < list.length; i++) {
if (String(list[i].name || "") === want) {
const logical = list[i].logical || ({});
return typeof logical.x === "number" ? logical.x : 1000000;
}
}
return 1000000;
}
// live grouping for the bar, groups ordered by physical output position.
function workspaceGroups() {
return groupWorkspaces(workspaces).slice().sort((a, b) => outputX(a.output) - outputX(b.output));
}
// Local gate for the grouping logic (untestable on a single monitor): feed
// a synthetic two-output array and assert grouping, per-tray idx sort, and
// empty-trailing-workspace filtering. Drive via the niriSelfTest IPC.
function selfTest() {
const failures = [];
const sample = [
{
"idx": 4,
"output": "DP-2",
"name": "browser"
},
{
"idx": 5,
"output": "DP-2",
"name": "vault"
},
{
"idx": 3,
"output": "DP-1",
"name": "games"
},
{
"idx": 6,
"output": "DP-1",
"name": null
},
{
"idx": 2,
"output": "DP-1",
"name": "media"
},
{
"idx": 1,
"output": "DP-1",
"name": "code",
"is_focused": true
}
];
const groups = groupWorkspaces(sample);
if (groups.length !== 2)
failures.push(`expected 2 groups, got ${groups.length}`);
const dp1 = groups.filter(g => g.output === "DP-1")[0] || null;
const dp2 = groups.filter(g => g.output === "DP-2")[0] || null;
if (!dp1)
failures.push("missing DP-1 group");
if (!dp2)
failures.push("missing DP-2 group");
if (dp1) {
const idxs = JSON.stringify(dp1.workspaces.map(w => w.idx));
if (idxs !== "[1,2,3]")
failures.push(`DP-1 idx order ${idxs} != [1,2,3]`);
}
if (dp2) {
const idxs = JSON.stringify(dp2.workspaces.map(w => w.idx));
if (idxs !== "[4,5]")
failures.push(`DP-2 idx order ${idxs} != [4,5]`);
}
const total = groups.reduce((sum, g) => sum + g.workspaces.length, 0);
if (total !== 5)
failures.push(`expected 5 pills after empty filter, got ${total}`);
try {
const focused = focusedOutputNameFromWorkspaces(sample);
if (focused !== "DP-1")
failures.push(`focused output ${focused} != DP-1`);
} catch (error) {
failures.push(`focused output helper missing: ${error}`);
}
try {
const shortcuts = parseWorkspaceShortcuts('binds {\n Mod+1 { focus-workspace "code" }\n Mod+4 { focus-workspace "browser" }\n Mod+Shift+4 { move-column-to-workspace "browser" }\n}');
if (shortcuts.code !== "1" || shortcuts.browser !== "4")
failures.push(`workspace shortcuts ${JSON.stringify(shortcuts)} != code/browser 1/4`);
} catch (error) {
failures.push(`workspace shortcut parser missing: ${error}`);
}
return {
"pass": failures.length === 0,
"failures": failures
};
}
function parsePickedColor(text) {
const value = String(text || "");
const match = value.match(/#[0-9a-fA-F]{6}/);
lastPickedColor = match ? match[0].toLowerCase() : "";
lastError = value.trim();
}
function windowById(id) {
if (id === null || id === undefined)
return null;
for (let i = 0; i < windows.length; i++) {
if (windows[i].id === id)
return windows[i];
}
return null;
}
function windowActions(id) {
return [
{
"id": "focus",
"label": "focus",
"route": "Niri.qml",
"niriAction": "focus-window",
"destructive": false,
"keyboard": "Enter"
},
{
"id": "toggle-fullscreen",
"label": "fullscreen",
"route": "Niri.qml",
"niriAction": "fullscreen-window",
"destructive": false,
"keyboard": "F"
},
{
"id": "move-workspace",
"label": "move workspace",
"route": "Niri.qml",
"niriAction": "move-window-to-workspace",
"destructive": false,
"keyboard": "M"
},
{
"id": "close-window",
"label": "close",
"route": "Niri.qml",
"niriAction": "close-window",
"destructive": true,
"keyboard": "Delete"
}
].map(action => {
return Object.assign({}, action, {
"windowId": id
});
});
}
function windowPanelContract() {
return {
"source": "Nuguland WindowInfo.qml Details.qml Buttons.qml plus Nuguland TaskViewContent.qml",
"ownedBy": "PopoutHost.qml inline WindowPanel",
"niriWindowCardPanel": true,
"previewSlotCard": true,
"detailsRows": true,
"ruleHintRows": true,
"windowActionRail": true,
"workspaceMoveAffordance": true,
"serviceActionsOnly": true,
"route": "Niri.qml",
"realNiriWindowState": true,
"cardParts": ["preview-slot", "details", "rule-hints", "actions", "workspace-move"],
"noCompositorOverviewClone": true
};
}
function windowCardObject() {
const win = focusedWindowRecord || ({});
const appId = win.app_id || win.appId || "";
const title = win.title || "";
const workspace = win.workspace_id ?? win.workspaceId ?? null;
const id = win.id ?? focusedWindow ?? null;
const hints = [];
if (appId.length > 0)
hints.push(`match app-id=${JSON.stringify(appId)}`);
if (title.length > 0)
hints.push(`match title=${JSON.stringify(title)}`);
if (workspace !== null)
hints.push(`workspace id ${workspace}`);
const actions = windowActions(id);
return {
"id": id,
"appId": appId,
"title": title,
"workspace": workspace,
"focused": focusedWindowRecord !== null,
"ruleHints": hints,
"previewSlot": {
"source": "niri-toplevel",
"available": focusedWindowRecord !== null,
"live": focusedWindowRecord !== null,
"title": title,
"appId": appId,
"role": "content.normal"
},
"actionRow": {
"route": "Niri.qml",
"keyboardFirst": true,
"pointerParity": true,
"actions": actions
},
"windowPanelContract": windowPanelContract(),
"detailsRows": [
{
"id": "app-id",
"label": "app-id",
"value": appId,
"role": "content.normal"
},
{
"id": "title",
"label": "title",
"value": title,
"role": "content.normal"
},
{
"id": "workspace",
"label": "workspace",
"value": workspace === null ? "unknown" : String(workspace),
"role": "ui.accent"
},
{
"id": "focused",
"label": "focused",
"value": focusedWindowRecord !== null ? "yes" : "no",
"role": focusedWindowRecord !== null ? "ui.accent" : "ui.minor"
}
],
"actions": actions,
"lastWindowAction": lastWindowAction
};
}
function windowRuleMetadata() {
return JSON.stringify(windowCardObject());
}
function windowCardAction(kind) {
const name = String(kind);
const card = windowCardObject();
const actionRows = card.actions || [];
let selected = null;
for (let i = 0; i < actionRows.length; i++)
if (actionRows[i].id === name) {
selected = actionRows[i];
}
lastWindowAction = {
"kind": name,
"route": "Niri.qml",
"accepted": selected !== null,
"niriAction": selected ? selected.niriAction : "",
"windowId": card.id,
"executed": false
};
return JSON.stringify(windowCardObject());
}
function stateJson() {
return JSON.stringify({
"available": available,
"ready": ready,
"workspaceCount": workspaces.length,
"windowCount": windows.length,
"windows": windows,
"outputCount": Object.keys(outputs).length,
"outputError": outputError,
"outputs": outputs,
"workspaces": workspaces,
"workspaceShortcuts": workspaceShortcuts,
"overviewState": overviewState,
"focusedWindow": focusedWindow,
"focusedOutputName": focusedOutputName,
"focusedAppId": focusedAppId,
"focusedTitle": focusedTitle,
"lastError": lastError
});
}
function applyEvent(line) {
if (streamFailed || String(line).trim().length === 0)
return;
try {
const patch = NiriState.reduce(root, JSON.parse(line));
if (patch === null)
return;
if (Object.prototype.hasOwnProperty.call(patch, "screenshotPath")) {
screenshotCaptured(patch.screenshotPath);
return;
}
for (const key of Object.keys(patch))
root[key] = patch[key];
ready = windowsInitialized && workspacesInitialized && overviewState !== "unknown";
if (ready)
retryDelay = 250;
} catch (error) {
failStream(String(error));
}
}
onReadyChanged: if (ready)
refreshOutputs()
Component.onCompleted: {
refreshOutputs();
refresh();
}
Connections {
target: Quickshell
function onScreensChanged() {
Qt.callLater(root.refreshOutputs);
}
}
// Niri has no OutputsChanged event. Qt screen notifications cover hotplug;
// polling also catches compositor-only geometry changes. One query at a time.
Timer {
interval: 5000
running: root.available
repeat: true
onTriggered: root.refreshOutputs()
}
Timer {
id: reconnect
onTriggered: root.refresh()
}
Timer {
interval: 5000
running: query.running && !root.ready && !root.streamFailed
onTriggered: root.failStream("event stream initialization timed out")
}
Timer {
interval: 3000
running: outputQuery.running
onTriggered: outputQuery.signal(9)
}
FileView {
id: keybindFile
path: root.keybindConfigPath
preload: true
printErrors: false
onLoaded: root.reloadWorkspaceShortcuts()
onLoadFailed: root.workspaceShortcuts = ({})
}
Process {
id: outputQuery
onRunningChanged: {
if (!running) {
if (!root.outputsFinished)
root.outputError = "outputs query failed to start";
if (root.outputsPending)
Qt.callLater(root.refreshOutputs);
}
}
stdout: StdioCollector {
id: outputStdout
waitForEnd: true
}
}
Process {
id: query
running: false
onRunningChanged: if (!running)
root.streamDisconnected()
stdout: SplitParser {
onRead: data => {
return root.applyEvent(data);
}
}
stderr: StdioCollector {
onStreamFinished: {
if (this.text.trim().length > 0 && !root.streamFailed)
root.lastError = this.text.trim();
}
}
}
Connections {
target: outputQuery
function onExited(code, status) {
root.outputsFinished = true;
root.applyOutputs(outputStdout.text, code, status);
}
}
Connections {
target: query
function onExited(code) {
if (!root.streamFailed)
root.lastError = `event stream exited ${code}`;
}
}
Process {
id: actionProc
onRunningChanged: if (!running)
root.dispatchNextAction()
stderr: StdioCollector {
onStreamFinished: root.lastError = this.text
}
}
Process {
id: colorProc
stdout: StdioCollector {
onStreamFinished: root.parsePickedColor(this.text)
}
stderr: StdioCollector {
onStreamFinished: root.lastError = this.text
}
}
}