An open popout only closed on a backdrop click, Escape, a toggle, or another screen claiming the slot. Focus moving away without a click — Alt+Tab, a focus keybind, a workspace switch, a newly mapped window — left it standing over the desktop. Three signals, all needed: - the host surface losing the keyboard, read from Qt's Window.active (PanelWindow exports no focus state); the only one that catches a focus grab by another layer surface - niri focusing an actual window, filtered on id >= 0: niri drops window focus to none while the popout holds the keyboard, so reacting to every change would make each open close itself - the focused workspace changing, via the new Niri.focusedWorkspaceId; workspace ids are unique across outputs, so a monitor switch counts too No exception for pkexec/keyring prompts raised by popout actions: they take the focus, so the popout closes and the new state shows on the next open. Rationale and measurements in DECISIONS.md.
135 lines
4.9 KiB
QML
135 lines
4.9 KiB
QML
// ABOUTME: Niri compositor IPC service — consumes the NIRI_SOCKET event stream.
|
|
// ABOUTME: Exposes reactive workspaces and the focused window for the bar widgets.
|
|
pragma Singleton
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
readonly property string socketPath: Quickshell.env("NIRI_SOCKET") || ""
|
|
|
|
// Reactive state consumed by widgets.
|
|
property var workspaces: []
|
|
property var windowsById: ({})
|
|
property int focusedWindowId: -1
|
|
property var activeWindow: null
|
|
|
|
// Connector name of the monitor holding the focused workspace ("" until known). Window-independent:
|
|
// an empty focused workspace still carries is_focused, so this resolves even with nothing on screen.
|
|
// Used to route keybind-triggered popouts to the active monitor (matches Quickshell screen.name).
|
|
readonly property string activeOutput: {
|
|
const w = root.workspaces.find(x => x && x.is_focused);
|
|
return w ? w.output : "";
|
|
}
|
|
|
|
// Id of the focused workspace (-1 until known). Ids are unique across outputs, so a change here
|
|
// covers both a workspace switch and a monitor switch — which is what popouts close on.
|
|
readonly property int focusedWorkspaceId: {
|
|
const w = root.workspaces.find(x => x && x.is_focused);
|
|
return w ? w.id : -1;
|
|
}
|
|
|
|
function recomputeActive() {
|
|
root.activeWindow = (root.focusedWindowId >= 0 && root.windowsById[root.focusedWindowId])
|
|
? root.windowsById[root.focusedWindowId]
|
|
: null;
|
|
}
|
|
|
|
// Event stream: niri sends the full current state up-front, then incremental updates.
|
|
Socket {
|
|
id: eventSock
|
|
path: root.socketPath
|
|
connected: root.socketPath.length > 0
|
|
|
|
parser: SplitParser {
|
|
splitMarker: "\n"
|
|
onRead: line => root.handleEvent(line)
|
|
}
|
|
|
|
onConnectedChanged: {
|
|
if (connected)
|
|
write("\"EventStream\"\n");
|
|
}
|
|
}
|
|
|
|
function handleEvent(line) {
|
|
if (!line || line.length === 0)
|
|
return;
|
|
var ev;
|
|
try {
|
|
ev = JSON.parse(line);
|
|
} catch (e) {
|
|
return;
|
|
}
|
|
|
|
if (ev.WorkspacesChanged) {
|
|
root.workspaces = ev.WorkspacesChanged.workspaces;
|
|
} else if (ev.WorkspaceActivated) {
|
|
var a = ev.WorkspaceActivated;
|
|
var target = root.workspaces.find(w => w.id === a.id);
|
|
if (target) {
|
|
var out = target.output;
|
|
root.workspaces = root.workspaces.map(function (w) {
|
|
var n = Object.assign({}, w);
|
|
if (w.output === out)
|
|
n.is_active = (w.id === a.id);
|
|
if (a.focused)
|
|
n.is_focused = (w.id === a.id);
|
|
return n;
|
|
});
|
|
}
|
|
} else if (ev.WorkspaceUrgencyChanged) {
|
|
var u = ev.WorkspaceUrgencyChanged;
|
|
root.workspaces = root.workspaces.map(w => w.id === u.id ? Object.assign({}, w, {
|
|
is_urgent: u.urgent
|
|
}) : w);
|
|
} else if (ev.WorkspaceActiveWindowChanged) {
|
|
var c = ev.WorkspaceActiveWindowChanged;
|
|
root.workspaces = root.workspaces.map(w => w.id === c.workspace_id ? Object.assign({}, w, {
|
|
active_window_id: c.active_window_id
|
|
}) : w);
|
|
} else if (ev.WindowsChanged) {
|
|
var map = {};
|
|
for (var i = 0; i < ev.WindowsChanged.windows.length; i++) {
|
|
var win = ev.WindowsChanged.windows[i];
|
|
map[win.id] = win;
|
|
}
|
|
root.windowsById = map;
|
|
var focused = ev.WindowsChanged.windows.find(w => w.is_focused);
|
|
root.focusedWindowId = focused ? focused.id : -1;
|
|
root.recomputeActive();
|
|
} else if (ev.WindowOpenedOrChanged) {
|
|
var w2 = ev.WindowOpenedOrChanged.window;
|
|
var m2 = Object.assign({}, root.windowsById);
|
|
m2[w2.id] = w2;
|
|
root.windowsById = m2;
|
|
if (w2.is_focused)
|
|
root.focusedWindowId = w2.id;
|
|
root.recomputeActive();
|
|
} else if (ev.WindowClosed) {
|
|
var m3 = Object.assign({}, root.windowsById);
|
|
delete m3[ev.WindowClosed.id];
|
|
root.windowsById = m3;
|
|
if (root.focusedWindowId === ev.WindowClosed.id)
|
|
root.focusedWindowId = -1;
|
|
root.recomputeActive();
|
|
} else if (ev.WindowFocusChanged) {
|
|
var fid = ev.WindowFocusChanged.id;
|
|
root.focusedWindowId = (fid === null || fid === undefined) ? -1 : fid;
|
|
root.recomputeActive();
|
|
}
|
|
}
|
|
|
|
// Focus a workspace by its niri per-output index. Best-effort for the pilot.
|
|
function focusWorkspace(idx) {
|
|
focusProc.command = ["niri", "msg", "action", "focus-workspace", String(idx)];
|
|
focusProc.running = true;
|
|
}
|
|
|
|
Process {
|
|
id: focusProc
|
|
}
|
|
}
|