three-finger tap opens a window sheet, not the overview
This commit is contained in:
parent
3aea8398a9
commit
bf8bc013ef
21 changed files with 1476 additions and 49 deletions
174
surfaces/quickshell/services/ViewtopControl.qml
Normal file
174
surfaces/quickshell/services/ViewtopControl.qml
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue