rescue: preserve unversioned source copy found on archdev
/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.
This commit is contained in:
parent
517d8c1927
commit
c367a4dbd9
11 changed files with 11672 additions and 0 deletions
|
|
@ -0,0 +1,307 @@
|
|||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue