Watch
1
0
Fork
You've already forked souveraine
0

three-finger tap opens a window sheet, not the overview

This commit is contained in:
Fimeg 2026-08-05 14:12:22 -04:00
commit bf8bc013ef
21 changed files with 1476 additions and 49 deletions

View file

@ -0,0 +1,174 @@
// The shell's door into viewtop's control socket.
//
// The compositor serves one JSON object per line `{"op":}` in,
// `{"ok":true,}` or `{"ok":false,"code":,"reason":}` back the same house
// grammar sessiond speaks, so this is shaped like SessiondPolicy rather than
// inventing a second idea of what talking to a daemon looks like.
//
// **This is the only place the shell addresses the scene.** Every window verb
// the hand can reach close, kill, place, pose, raise, focus goes through
// `scene()`. The alternative is what the dial does today: `hyprctl dispatch`
// baked into a surface, which stopped existing under viewtop and took the
// dial's "Kill window" with it. A verb spelled out inside a widget is a verb
// that dies when the compositor changes.
//
// A short-lived connection per request, deliberately. viewtop's socket is
// request/response and holds no session; there is no heartbeat to protect here
// the way SessiondBridge's EOF is load-bearing, so nothing is gained by
// keeping it open and a dropped long-lived socket would need reconnect logic
// to be correct.
//
// Failures are LOUD. A window verb that silently did nothing is exactly the
// bug this exists to close: `overview toggle` shelled out to a handler that
// did not exist and the tap did nothing, silently, for weeks.
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
// The last refusal, for a surface that wants to show one. `close` is a
// request the client may refuse; the hand deserves to see that rather than
// watch nothing happen.
property string lastError: ""
signal refused(string intent, string reason)
signal succeeded(string intent)
// Requests waiting on a connection. A queue rather than SessiondPolicy's
// single slot: the sheet can fire two verbs in a row (kill after a close
// that was refused), and dropping the second would be silent.
property var _queue: []
property var _inflight: null
function _send(msg) {
root._queue.push(msg);
if (sock.connected)
root._drain();
else
sock.connected = true;
}
function _drain() {
if (root._inflight !== null || root._queue.length === 0)
return;
root._inflight = root._queue.shift();
sock.write(JSON.stringify(root._inflight) + "\n");
}
// Address the scene. `intent` is one of the compositor's own the table it
// returns from `{"op":"describe"}` is authoritative, and this deliberately
// does not keep a second copy of it to validate against.
function scene(intent, args) {
const msg = { op: "scene", intent: intent };
for (const k in args)
msg[k] = args[k];
root._send(msg);
}
// Ask the client to close. It may refuse or prompt; that is the protocol,
// not a bug, and `refused` carries it.
function close(id) {
root.scene("close", { id: id });
}
// End it regardless. The floor under close(), for a client that is hung or
// says no "two apps and no way to turn them off" is what this answers.
// Unsaved work is lost, so a surface offering this should mean it.
function kill(id) {
root.scene("kill", { id: id });
}
// Position and size a window. The shell owns layout; the compositor owns
// whether a placement is legal and will refuse one that is not.
function place(id, x, y, width, height) {
root.scene("place", {
id: id,
at: { x: x, y: y },
size: { width: width, height: height }
});
}
// Give a window back to the layout.
function unplace(id) {
root.scene("unplace", { id: id });
}
// Compose a window's visual geometry the verb that makes a live app a
// scaled, floating, still-touchable thing rather than a picture of one.
// The compositor maps input back through the inverse, so a shrunken window
// still receives touch where it is drawn.
function pose(id, scale, rotation, anchorX, anchorY) {
root.scene("pose", {
id: id,
scale: scale,
rotation: rotation ?? 0.0,
anchor: { x: anchorX ?? 0.5, y: anchorY ?? 0.5 }
});
}
function raise(id) {
root.scene("raise", { id: id });
}
function focus(id) {
root.scene("focus", { id: id });
}
Socket {
id: sock
path: Quickshell.env("XDG_RUNTIME_DIR") + "/souveraine/viewtop.sock"
onConnectionStateChanged: {
if (connected) {
root._drain();
} else if (root._inflight !== null || root._queue.length > 0) {
const intent = (root._inflight && root._inflight.intent) || "?";
root.lastError = "viewtop socket unavailable";
console.error("[viewtop-control] could not reach the compositor at "
+ sock.path + " — the scene was NOT changed");
root._inflight = null;
root._queue = [];
root.refused(intent, root.lastError);
}
}
parser: SplitParser {
splitMarker: "\n"
onRead: message => {
const sent = root._inflight;
const intent = (sent && sent.intent) || "?";
root._inflight = null;
let reply;
try {
reply = JSON.parse(message);
} catch (e) {
console.error("[viewtop-control] unparseable reply: " + message);
root.lastError = "unparseable reply";
root.refused(intent, root.lastError);
root._drain();
return;
}
if (reply.ok !== true) {
// `gone` is the ordinary one: the window closed between the
// sheet opening and the button being pressed. Still a
// refusal, still surfaced, because a sheet acting on a dead
// id should say so rather than appear to work.
const why = reply.reason || reply.code || "refused without a reason";
console.log("[viewtop-control] " + intent + " refused: " + why);
root.lastError = why;
root.refused(intent, why);
} else {
root.lastError = "";
root.succeeded(intent);
}
root._drain();
}
}
}
}