Watch
1
0
Fork
You've already forked souveraine
0
souveraine/surfaces/quickshell/services/DeviceEvidence.qml
Fimeg 0bebc779c6 Selection menu surface: chip that expands to actions (TASK-18)
Overlay layer following DialHost, keyboardFocus None throughout, and an input
mask limited to the card so taps elsewhere reach the app underneath. Anchors to
the pointer hint above the touch point, clamped on screen.

Chip shows a character count, never a preview: the surface floats over the app
that owns the selection and the content may be a password, so it stays ambient
and discloses nothing. Read Aloud is live via Speech; agent and reference
actions render with the reason they cannot act yet.

Adds DeviceEvidence, reporting input to the state machine's existing
Request::Input with an intent label, so a new input surface is not another
isolated actor per DEVICE-STATE-MACHINE 1. Opt-in: nothing loads or watches
until Config.options.selection.enable.
2026-07-29 17:47:20 -04:00

130 lines
4.7 KiB
QML

pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
/**
* DeviceEvidence — the shell's ingress to the device state machine.
*
* DEVICE-STATE-MACHINE §1 is a list of seven actors that each saw one facet of
* the device and could not see the others. Every shell surface that takes user
* input is a candidate for becoming an eighth. This singleton exists so that
* "report that the user did something" is one line, and a new surface has no
* excuse to be an isolated unknown.
*
* The wire is `Request::Input { trigger }` (souveraine/src/sessiond/protocol.rs
* — that file is the contract): `{"op":"input","trigger":"touch"}`. It resets
* the idle budget the lock/blank rules count against, which is what lets the
* machine distinguish "the user is looking at this" from "this has been lit for
* ten minutes."
*
* `intent` rides along in the Envelope. protocol.rs is explicit that it is
* "declared, never verified... evidence in exactly the sense doctrine §9 means —
* useful for reconstruction, never a basis for a decision. Nothing branches on
* it." So it is safe to be honest in, and it is what §11 wants recorded: the
* intent, not only the leaf. Never put user content in it — a surface name, not
* what the surface was showing.
*
* No sessiond on the socket (laptop, or bring-up) = every call no-ops quietly.
* This is evidence, not an authority: a dropped report must never be an error
* the user sees.
*/
Singleton {
id: root
// Reports are coalesced: a keyboard would otherwise emit one request per
// keystroke to reset a budget measured in tens of seconds. The machine only
// needs to know the user is still there.
readonly property int _throttleMs: 2000
property double _lastSentAt: 0
property string _pendingTrigger: ""
property string _pendingIntent: ""
/**
* Report real user input.
*
* trigger: "touch" | "key" | "power_button" | "double_tap_to_wake"
* | "squeeze" | "unknown" (InputTrigger, snake_case)
* intent: short surface label, e.g. "selection-menu". No user content.
*/
function report(trigger, intent) {
const t = String(trigger ?? "unknown");
const now = Date.now();
if (now - root._lastSentAt < root._throttleMs) {
// Keep the newest label; the budget reset is idempotent so dropping
// the intervening reports costs nothing.
root._pendingTrigger = t;
root._pendingIntent = String(intent ?? "");
flushTimer.running = true;
return;
}
root._send(t, String(intent ?? ""));
}
/** Convenience for the common case: a tap on one of our own surfaces. */
function touched(intent) {
root.report("touch", intent);
}
Timer {
id: flushTimer
interval: root._throttleMs
repeat: false
onTriggered: {
if (root._pendingTrigger.length === 0) return;
root._send(root._pendingTrigger, root._pendingIntent);
root._pendingTrigger = "";
root._pendingIntent = "";
}
}
property var _queued: null
function _send(trigger, intent) {
root._lastSentAt = Date.now();
const msg = { op: "input", trigger: trigger };
if (intent.length > 0) msg.intent = intent;
if (sock.connected) {
sock.write(JSON.stringify(msg) + "\n");
return;
}
root._queued = msg;
sock.connected = true;
}
Socket {
id: sock
path: Quickshell.env("XDG_RUNTIME_DIR") + "/souveraine/sessiond.sock"
onConnectionStateChanged: {
if (connected && root._queued) {
const m = root._queued;
root._queued = null;
sock.write(JSON.stringify(m) + "\n");
} else if (!connected && root._queued) {
// Quiet on purpose. Evidence is best-effort; a missing daemon is
// the laptop's normal state and must not look like a fault.
root._queued = null;
}
}
parser: SplitParser {
splitMarker: "\n"
onRead: message => {
// Nothing to do with a reply — this is fire-and-forget. Only a
// refusal is worth a line, so a protocol drift is not silent.
try {
const reply = JSON.parse(message);
if (reply.ok !== true)
console.log("[device-evidence] refused:",
reply.code ?? "?", reply.reason ?? "");
} catch (e) {
// Malformed reply is not worth escalating for a fire-and-forget.
}
}
}
}
}