/home/casey/souveraine on the build box is a full copy of this repo with no
.git at all, newest sources 2026-08-09 21:07. Audited by hashing every file
and querying this repo's object database: 1545 checked, ten unknown after
excluding the vendored upower submodule.
Eight carry real deltas; roughly fifty substantive lines have no upstream
equivalent. They are the follow-up to b2774c8 that was never committed: a
logind capability query before firing a power verb, and a failure-path release
of the in-flight power latch without which a refused suspend deafens the verb
until the next boot.
Preserved verbatim, not merged. The tree has moved 100+ commits and the
sessiond power seam wants a human.
307 lines
9.6 KiB
QML
307 lines
9.6 KiB
QML
/*
|
|
* Copyright (C) 2026 Casey Tunturi
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
// One live hand between the face and usb-hid-inject.
|
|
//
|
|
// The page never opens /dev/hidg* and never spawns a process per pointer
|
|
// report. This singleton owns one streaming helper while USB Hands is joined,
|
|
// batches motion to the display clock, and exposes the same verbs over
|
|
// QuickShell IPC so the active Souveraine agent can use the hand Casey opened.
|
|
// Configfs and role changes remain sessiond -> usb-signaller business.
|
|
pragma Singleton
|
|
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import qs
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
property bool active: false
|
|
property bool ready: false
|
|
property string lastError: ""
|
|
|
|
readonly property bool modeReady: UsbState.mode === "hid" || UsbState.mode === "kvm"
|
|
readonly property bool permitted: root.active && !GlobalStates.screenLockSecure
|
|
readonly property bool canInject: root.permitted && root.modeReady && root.ready
|
|
|
|
// Loaded only while this surface is joined. It tells the agent what the
|
|
// newly-present hand can do without teaching every ordinary conversation
|
|
// about a USB gadget it is not using.
|
|
readonly property string skill: "USB Hands is joined to this conversation. "
|
|
+ "Casey is using the phone as a trackpad and voice-first controller for an attached host. "
|
|
+ "When he asks you to type or press a key there, use the existing bridge: "
|
|
+ "`qs -c souveraine ipc call usbHands sendText TEXT`, "
|
|
+ "`qs -c souveraine ipc call usbHands tap KEY`, "
|
|
+ "`qs -c souveraine ipc call usbHands key KEY MODIFIERS`, or "
|
|
+ "`qs -c souveraine ipc call usbHands click left|right|middle`. "
|
|
+ "Ask `qs -c souveraine ipc call usbHands status` rather than assuming the cable is armed. "
|
|
+ "Do not inject anything Casey did not ask to send to the attached host."
|
|
|
|
signal controllerChanged()
|
|
signal refused(string reason)
|
|
|
|
function open(): bool {
|
|
if (GlobalStates.screenLockSecure) {
|
|
root.lastError = "Unlock before giving the glass a hand on another machine";
|
|
root.refused(root.lastError);
|
|
return false;
|
|
}
|
|
root.lastError = "";
|
|
root.active = true;
|
|
UsbState.refresh();
|
|
root._reconcile();
|
|
root.controllerChanged();
|
|
return true;
|
|
}
|
|
|
|
function close() {
|
|
if (root.ready) {
|
|
root._flushPointer();
|
|
injector.write("release\n");
|
|
}
|
|
root.active = false;
|
|
root.ready = false;
|
|
root._pendingX = 0;
|
|
root._pendingY = 0;
|
|
root._pendingWheel = 0;
|
|
flushTimer.stop();
|
|
root._reconcile();
|
|
root.controllerChanged();
|
|
}
|
|
|
|
function arm() {
|
|
if (!root.active) {
|
|
root.lastError = "USB Hands is not joined";
|
|
root.refused(root.lastError);
|
|
root.controllerChanged();
|
|
return false;
|
|
}
|
|
if (!UsbState.hasMode("hid_mode")) {
|
|
root.lastError = "The installed gadget does not advertise HID mode";
|
|
root.refused(root.lastError);
|
|
root.controllerChanged();
|
|
return false;
|
|
}
|
|
UsbState.setMode("hid");
|
|
return true;
|
|
}
|
|
|
|
function retry() {
|
|
root.lastError = "";
|
|
if (injector.running)
|
|
injector.running = false;
|
|
root._reconcile();
|
|
}
|
|
|
|
function _reconcile() {
|
|
const wanted = root.permitted && root.modeReady;
|
|
if (!wanted) {
|
|
root.ready = false;
|
|
if (injector.running)
|
|
injector.running = false;
|
|
return;
|
|
}
|
|
if (!injector.running) {
|
|
root.ready = false;
|
|
injector.running = true;
|
|
}
|
|
}
|
|
|
|
function _notReadyReason(): string {
|
|
if (!root.active) return "USB Hands is not joined";
|
|
if (GlobalStates.screenLockSecure) return "The glass is locked";
|
|
if (!root.modeReady) return "Arm HID or KVM mode first";
|
|
if (root.lastError.length > 0) return root.lastError;
|
|
return "The HID bridge is still waking";
|
|
}
|
|
|
|
function _write(command): bool {
|
|
if (!root.canInject) {
|
|
root.lastError = root._notReadyReason();
|
|
root.refused(root.lastError);
|
|
root.controllerChanged();
|
|
return false;
|
|
}
|
|
injector.write(command + "\n");
|
|
return true;
|
|
}
|
|
|
|
function sendText(value): bool {
|
|
const lines = String(value ?? "").replace(/\r\n/g, "\n").split("\n");
|
|
if (lines.length === 1 && lines[0].length === 0)
|
|
return false;
|
|
for (let i = 0; i < lines.length; i++) {
|
|
if (lines[i].length > 0 && !root._write("type " + lines[i]))
|
|
return false;
|
|
if (i + 1 < lines.length && !root._write("key enter"))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function key(name, modifiers = ""): bool {
|
|
const keyName = String(name ?? "").trim();
|
|
const mods = String(modifiers ?? "").trim().replace(/[,]+/g, " ");
|
|
if (!/^[A-Za-z0-9]+$/.test(keyName)
|
|
|| (mods.length > 0 && !/^[A-Za-z ]+$/.test(mods))) {
|
|
root.lastError = "Key names and modifiers must be plain words";
|
|
root.refused(root.lastError);
|
|
root.controllerChanged();
|
|
return false;
|
|
}
|
|
return root._write("key " + keyName + (mods.length > 0 ? " " + mods : ""));
|
|
}
|
|
|
|
function click(button = "left"): bool {
|
|
const name = String(button ?? "left").toLowerCase();
|
|
if (!["left", "right", "middle"].includes(name)) {
|
|
root.lastError = "Unknown pointer button: " + name;
|
|
root.refused(root.lastError);
|
|
root.controllerChanged();
|
|
return false;
|
|
}
|
|
root._flushPointer();
|
|
return root._write("click " + name);
|
|
}
|
|
|
|
property int _pendingX: 0
|
|
property int _pendingY: 0
|
|
property int _pendingWheel: 0
|
|
|
|
function movePointer(x, y, wheel = 0): bool {
|
|
if (!root.canInject) {
|
|
root.lastError = root._notReadyReason();
|
|
root.refused(root.lastError);
|
|
root.controllerChanged();
|
|
return false;
|
|
}
|
|
root._pendingX += Math.round(Number(x));
|
|
root._pendingY += Math.round(Number(y));
|
|
root._pendingWheel += Math.round(Number(wheel));
|
|
if (!flushTimer.running)
|
|
flushTimer.start();
|
|
return true;
|
|
}
|
|
|
|
function _flushPointer() {
|
|
if (!root.ready)
|
|
return;
|
|
const x = root._pendingX;
|
|
const y = root._pendingY;
|
|
const wheel = root._pendingWheel;
|
|
root._pendingX = 0;
|
|
root._pendingY = 0;
|
|
root._pendingWheel = 0;
|
|
if (x !== 0 || y !== 0 || wheel !== 0)
|
|
root._write("pointer " + x + " " + y + " " + wheel + " 0");
|
|
}
|
|
|
|
Timer {
|
|
id: flushTimer
|
|
interval: 16
|
|
repeat: false
|
|
onTriggered: root._flushPointer()
|
|
}
|
|
|
|
Process {
|
|
id: injector
|
|
command: ["usb-hid-inject", "stream"]
|
|
stdinEnabled: true
|
|
|
|
stdout: SplitParser {
|
|
splitMarker: "\n"
|
|
onRead: line => {
|
|
if (line.trim() !== "ready")
|
|
return;
|
|
root.ready = true;
|
|
root.lastError = "";
|
|
root.controllerChanged();
|
|
}
|
|
}
|
|
|
|
stderr: SplitParser {
|
|
splitMarker: "\n"
|
|
onRead: line => {
|
|
const message = line.trim().replace(/^error:\s*/, "");
|
|
if (message.length === 0)
|
|
return;
|
|
root.lastError = message;
|
|
root.controllerChanged();
|
|
}
|
|
}
|
|
|
|
onExited: (exitCode, exitStatus) => {
|
|
root.ready = false;
|
|
if (root.permitted && root.modeReady && root.lastError.length === 0)
|
|
root.lastError = "HID bridge exited (" + exitCode + ")";
|
|
root.controllerChanged();
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: UsbState
|
|
|
|
function onRefreshed() {
|
|
root._reconcile();
|
|
root.controllerChanged();
|
|
}
|
|
|
|
function onChangeFailed(reason) {
|
|
root.lastError = reason;
|
|
root.controllerChanged();
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: GlobalStates
|
|
|
|
function onScreenLockSecureChanged() {
|
|
if (GlobalStates.screenLockSecure && root.active)
|
|
root.close();
|
|
}
|
|
}
|
|
|
|
onActiveChanged: root._reconcile()
|
|
onModeReadyChanged: {
|
|
root._reconcile();
|
|
root.controllerChanged();
|
|
}
|
|
|
|
IpcHandler {
|
|
target: "usbHands"
|
|
|
|
function status(): string {
|
|
return JSON.stringify({
|
|
active: root.active,
|
|
ready: root.ready,
|
|
mode: UsbState.mode,
|
|
error: root.lastError
|
|
});
|
|
}
|
|
|
|
function sendText(text: string): string {
|
|
return root.sendText(text) ? "sent" : root._notReadyReason();
|
|
}
|
|
|
|
function key(keyName: string, modifiers: string): string {
|
|
return root.key(keyName, modifiers) ? "sent" : root._notReadyReason();
|
|
}
|
|
|
|
function tap(keyName: string): string {
|
|
return root.key(keyName) ? "sent" : root._notReadyReason();
|
|
}
|
|
|
|
function click(button: string): string {
|
|
return root.click(button) ? "sent" : root._notReadyReason();
|
|
}
|
|
|
|
function pointer(x: int, y: int, wheel: int): string {
|
|
return root.movePointer(x, y, wheel) ? "sent" : root._notReadyReason();
|
|
}
|
|
}
|
|
}
|