Watch
1
0
Fork
You've already forked souveraine
0

hands: arm the wire before opening the pad

This commit is contained in:
Fimeg 2026-08-17 11:08:36 -04:00
commit 59b52a818b
7 changed files with 210 additions and 148 deletions

View file

@ -23,27 +23,31 @@
* she is still on screen while you work. Casey, 2026-08-15: *"I want her, and * she is still on screen while you work. Casey, 2026-08-15: *"I want her, and
* then below her to have like a trackpad zone / keyboard zone stuff."* * then below her to have like a trackpad zone / keyboard zone stuff."*
* *
* At 760/226 the split was the other way round and the controls had 226px to * At 760/226 the controls had only 226px. At 320/the-rest the trackpad ate
* share between a status row, an agent field, the pads and four buttons — so * nearly half the glass. This split keeps her proportions and gives the pad
* the pads were 92px and the last row sat under the dock. * one deliberate working area. The dock now yields only while this
* * conditional surface is open; the remaining reserve belongs to the system
* `--dock-reserve` is a number here because the dock claims **no exclusive * gesture rail, not the dock.
* zone** unless it is Pinned (`modules/ii/dock/Dock.qml:321`), so nothing
* shrinks this surface for it and the bottom row would be untappable. Delete
* this the day the dock yields on its own.
*/ */
:root { --dock-reserve: 84px; } :root { --gesture-rail-reserve: 32px; }
body.hid #live2d-widget { height: 320px; } body.hid #live2d-widget { height: 520px; }
/*
* Keep her proportions when the box shrinks. The canvas fills its element and
* the rig has no letterboxing of its own, so a short full-width box draws the
* same model squashed. The ratio is the full-height box's, 540:760.
*/
body.hid #live2d {
display: block; height: 100%; width: auto;
aspect-ratio: 540 / 760; margin: 0 auto;
}
body.hid #hands { body.hid #hands {
top: 326px; top: 526px;
height: auto; height: auto;
bottom: var(--dock-reserve); bottom: var(--gesture-rail-reserve);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
/* The pads take whatever the fixed rows leave, rather than a written-down 92. */ body.hid #track-row { flex: 1; height: auto; min-height: 180px; }
body.hid #track-row,
body.hid #hands.keyboard #keyboard-pane { flex: 1; height: auto; min-height: 92px; }
#live2d { width: 100%; height: 100%; } #live2d { width: 100%; height: 100%; }
#bubble { #bubble {
position: fixed; left: 8px; right: 8px; top: 12px; position: fixed; left: 8px; right: 8px; top: 12px;
@ -56,8 +60,7 @@
#bubble.up { opacity: 1; } #bubble.up { opacity: 1; }
/* /*
* The room viewtop left under her, given a purpose. Trackpad first; the * The room viewtop left under her, given a purpose. The dark sheer gradient
* literal keyboard only replaces it when summoned. The dark sheer gradient
* keeps the controls legible without putting an app-shaped slab around her. * keeps the controls legible without putting an app-shaped slab around her.
*/ */
#hands { #hands {
@ -100,20 +103,18 @@
#hands-listening { display: none; color: #e8b7d8; } #hands-listening { display: none; color: #e8b7d8; }
#hands.listening #hands-listening { display: inline; } #hands.listening #hands-listening { display: inline; }
#hands-status { flex: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: rgba(255,255,255,.68); } #hands-status { flex: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: rgba(255,255,255,.68); }
.thread-button { width: 30px; min-width: 30px !important; height: 26px !important; padding: 0 !important; border-radius: 10px !important; } #agent-form { height: 40px; display: flex; gap: 6px; }
#agent-input {
#agent-form, #host-form { height: 40px; display: flex; gap: 6px; }
#agent-input, #host-input {
min-width: 0; flex: 1; height: 40px; padding: 0 13px; min-width: 0; flex: 1; height: 40px; padding: 0 13px;
border: 1px solid rgba(255,255,255,.12); border-radius: 14px; border: 1px solid rgba(255,255,255,.12); border-radius: 14px;
outline: none; color: #fff; background: rgba(0,0,0,.20); outline: none; color: #fff; background: rgba(0,0,0,.20);
-webkit-user-select: text; user-select: text; -webkit-user-select: text; user-select: text;
} }
#agent-input:focus, #host-input:focus { border-color: rgba(217,180,235,.55); } #agent-input:focus { border-color: rgba(217,180,235,.55); }
#agent-input::placeholder, #host-input::placeholder { color: rgba(255,255,255,.43); } #agent-input::placeholder { color: rgba(255,255,255,.43); }
#mic.listening { color: #f0b6db; box-shadow: inset 0 0 0 1px rgba(240,182,219,.45); } #mic.listening { color: #f0b6db; box-shadow: inset 0 0 0 1px rgba(240,182,219,.45); }
#track-row, #keyboard-pane { height: 92px; margin-top: 6px; } #track-row { height: 92px; margin-top: 6px; }
#track-row { display: flex; gap: 6px; } #track-row { display: flex; gap: 6px; }
#trackpad, #scrollpad { #trackpad, #scrollpad {
position: relative; overflow: hidden; position: relative; overflow: hidden;
@ -133,12 +134,6 @@
content: '↕'; position: absolute; inset: 0; display: grid; place-items: center; content: '↕'; position: absolute; inset: 0; display: grid; place-items: center;
color: rgba(255,255,255,.35); font-size: 20px; color: rgba(255,255,255,.35); font-size: 20px;
} }
#keyboard-pane { display: none; }
#keyboard-pane .quick-keys { display: flex; gap: 5px; margin-top: 6px; }
#keyboard-pane .quick-keys button { flex: 1; height: 40px; padding: 0 5px; }
#hands.keyboard #track-row { display: none; }
#hands.keyboard #keyboard-pane { display: block; }
.hands-actions { height: 40px; display: flex; gap: 5px; margin-top: 6px; } .hands-actions { height: 40px; display: flex; gap: 5px; margin-top: 6px; }
.hands-actions button { flex: 1; height: 40px !important; padding: 0 5px !important; } .hands-actions button { flex: 1; height: 40px !important; padding: 0 5px !important; }
#cad.holding { #cad.holding {
@ -155,8 +150,6 @@
<span id="hands-dot"></span> <span id="hands-dot"></span>
<span id="hands-status">USB hand asleep</span> <span id="hands-status">USB hand asleep</span>
<span id="hands-listening">listening</span> <span id="hands-listening">listening</span>
<button class="thread-button" id="resume-thread" title="Resume latest conversation"></button>
<button class="thread-button" id="new-thread" title="Begin a new conversation"></button>
</div> </div>
<form id="agent-form"> <form id="agent-form">
<button type="button" id="mic" aria-label="Start or stop listening"></button> <button type="button" id="mic" aria-label="Start or stop listening"></button>
@ -168,24 +161,9 @@
<div id="trackpad" aria-label="Trackpad"></div> <div id="trackpad" aria-label="Trackpad"></div>
<div id="scrollpad" aria-label="Scroll"></div> <div id="scrollpad" aria-label="Scroll"></div>
</div> </div>
<div id="keyboard-pane">
<form id="host-form">
<input id="host-input" autocomplete="off" enterkeyhint="send"
placeholder="Type into the attached host…">
<button type="submit" aria-label="Type on host"></button>
</form>
<div class="quick-keys">
<button data-key="escape">Esc</button>
<button data-key="tab">Tab</button>
<button data-key="backspace"></button>
<button data-key="enter">Enter</button>
</div>
</div>
<div class="hands-actions"> <div class="hands-actions">
<button id="left-click">Left</button> <button id="left-click">Left</button>
<button id="right-click">Right</button> <button id="right-click">Right</button>
<button id="keyboard-toggle"></button>
<button id="arm-hid">Arm</button>
<button id="cad" title="Hold for Ctrl+Alt+Delete">CAD</button> <button id="cad" title="Hold for Ctrl+Alt+Delete">CAD</button>
</div> </div>
</section> </section>
@ -363,7 +341,7 @@
window.souveraine.hid(message); window.souveraine.hid(message);
} }
var agentPending = false, hostPending = false; var agentPending = false;
window.hands = { window.hands = {
state: function (state) { state: function (state) {
state = state || {}; state = state || {};
@ -386,8 +364,6 @@
: 'Tell ' + (state.agent || 'your agent') + ' what the other screen needs…'; : 'Tell ' + (state.agent || 'your agent') + ' what the other screen needs…';
hands.classList.toggle('thinking', !!state.thinking); hands.classList.toggle('thinking', !!state.thinking);
document.getElementById('agent-input').disabled = !!state.thinking || agentPending; document.getElementById('agent-input').disabled = !!state.thinking || agentPending;
document.getElementById('resume-thread').classList.toggle('on', state.conversation === 'resumed');
document.getElementById('new-thread').classList.toggle('on', state.conversation === 'new');
requestAnimationFrame(publishShape); requestAnimationFrame(publishShape);
}, },
agentResult: function (result) { agentResult: function (result) {
@ -397,14 +373,6 @@
input.disabled = hands.classList.contains('thinking'); input.disabled = hands.classList.contains('thinking');
if (result.accepted) input.value = ''; if (result.accepted) input.value = '';
else if (result.reason) document.getElementById('hands-status').textContent = result.reason; else if (result.reason) document.getElementById('hands-status').textContent = result.reason;
},
hostResult: function (result) {
result = result || {};
hostPending = false;
var input = document.getElementById('host-input');
input.disabled = false;
if (result.accepted) input.value = '';
else if (result.reason) document.getElementById('hands-status').textContent = result.reason;
} }
}; };
@ -422,13 +390,6 @@
if (!window.souveraine) return; if (!window.souveraine) return;
window.souveraine.talk(hands.classList.contains('listening') ? 'end' : 'start'); window.souveraine.talk(hands.classList.contains('listening') ? 'end' : 'start');
}); });
document.getElementById('resume-thread').addEventListener('click', function () {
if (window.souveraine) window.souveraine.thread('resume');
});
document.getElementById('new-thread').addEventListener('click', function () {
if (window.souveraine) window.souveraine.thread('new');
});
var trackpad = document.getElementById('trackpad'); var trackpad = document.getElementById('trackpad');
var trackDown = false, trackX = 0, trackY = 0, trackTravel = 0, trackAt = 0; var trackDown = false, trackX = 0, trackY = 0, trackTravel = 0, trackAt = 0;
trackpad.addEventListener('pointerdown', function (event) { trackpad.addEventListener('pointerdown', function (event) {
@ -478,28 +439,6 @@
document.getElementById('right-click').addEventListener('click', function () { document.getElementById('right-click').addEventListener('click', function () {
hid('click', { button: 'right' }); hid('click', { button: 'right' });
}); });
document.getElementById('arm-hid').addEventListener('click', function () { hid('arm'); });
var keyboardToggle = document.getElementById('keyboard-toggle');
keyboardToggle.addEventListener('click', function () {
var showing = !hands.classList.contains('keyboard');
hands.classList.toggle('keyboard', showing);
keyboardToggle.classList.toggle('on', showing);
var input = document.getElementById('host-input');
if (showing) input.focus(); else input.blur();
});
document.getElementById('host-form').addEventListener('submit', function (event) {
event.preventDefault();
var input = document.getElementById('host-input');
if (input.value.length === 0 || hostPending) return;
hostPending = true;
input.disabled = true;
hid('type', { text: input.value });
});
Array.prototype.forEach.call(document.querySelectorAll('[data-key]'), function (button) {
button.addEventListener('click', function () { hid('key', { key: button.dataset.key }); });
});
// Ctrl+Alt+Delete is the one quick key a stray tap must not send. Its hold // Ctrl+Alt+Delete is the one quick key a stray tap must not send. Its hold
// is local feedback; only the completed edge crosses to the HID owner. // is local feedback; only the completed edge crosses to the HID owner.
var cad = document.getElementById('cad'), cadTimer = null, cadPointer = null; var cad = document.getElementById('cad'), cadTimer = null, cadPointer = null;

View file

@ -22,6 +22,7 @@ How Souveraine is built, and why. For whoever reads it, human or agent. When thi
**surface**: how the body she lives in moves **surface**: how the body she lives in moves
- [motion](surface/01-motion.md). One clock, two strategies, and why the shell never transforms a window. - [motion](surface/01-motion.md). One clock, two strategies, and why the shell never transforms a window.
- [session start](surface/02-session-start.md). One package-owned dependency graph from compositor socket to authority to shell. - [session start](surface/02-session-start.md). One package-owned dependency graph from compositor socket to authority to shell.
- [USB Hands](surface/03-usb-hands.md). An armed wire, an optional trackpad, and one acknowledged hand into the attached host.
Coming: the rest of identity (the memory filesystem, the ledgers), the nervous system (schedules, the event log), the consciousness cycle, the runtime, federation, a glossary. Coming: the rest of identity (the memory filesystem, the ledgers), the nervous system (schedules, the event log), the consciousness cycle, the runtime, federation, a glossary.

122
saf/surface/03-usb-hands.md Normal file
View file

@ -0,0 +1,122 @@
# USB Hands
The phone may become a keyboard and pointer for the host on its USB cable.
That reach has three states with three owners; collapsing them is what made the
first surface confusing.
## The wire, the surface, the hand
`UsbState.mode` says what sessiond and usb-signaller have made the port. HID or
KVM means the wire is armed. Developer or charging-only means it is not.
QuickShell never writes configfs and the page never opens `/dev/hidg*`.
`HidController.active` says the trackpad surface is open. It is not another
name for an armed port. Power Options is the human place that arms or disarms
the wire; a separate child action opens or closes the trackpad, and opening is
refused until HID or KVM is already armed.
`HidController.ready` says its one persistent `usb-hid-inject stream` helper
has acknowledged the endpoint. Pointer motion is batched to the display clock;
text, keys, clicks, and release reports go through that one writer.
These are deliberately different facts. A composed gadget with no open
surface is safe and useful. An open surface with no composed gadget is a dead
control and is refused. A helper process that has not said `ready` is not
dressed up as a usable hand.
The resulting state graph is small:
```
developer/charging -> no trackpad
HID or KVM armed -> trackpad may be opened
trackpad active -> dock yields; Face routes gestures and words
disarm or lock -> helper releases, trackpad closes
```
Arming the wire alone changes no shell furniture. The dock leaves only while
the conditional trackpad is actually on the glass.
## Authority and mechanism
The path has one owner at each layer:
- QuickShell asks sessiond for `set_usb_mode`; `UsbState` only projects the
answer and never writes the gadget.
- Sessiond records the USB mode action and asks usb-signaller to compose the
port. The supported postures are developer, HID, KVM, and charging-only.
- `HidController` alone owns the live stream helper. The Face page emits
intent into that controller; it never opens `/dev/hidg*` itself.
- The substrate's `body hand` verb returns to QuickShell's `usbHands` IPC. It
does not create a second HID writer merely because the caller is an agent.
The present route is not yet the final authority seam. `attached_identity`
and `probe_owner` are explicit unknowns, and a locally permitted caller can
still reach usb-signaller's system D-Bus mechanism around sessiond. Joining an
unlocked surface and arming it in Power Options are useful local gates, not a
credential or known-peer policy. The seam is complete only when the mutation
mechanism admits the attested/sessiond path alone and sessiond can distinguish
a known scoped peer from an unknown attachment. Charging-only remains the
always-available safe contraction.
Mode failure must return a refusal and leave or restore a usable USB posture.
It must not strand the management lane, a bound UDC, a held key, or a pointer
button. Closing Hands, disarming, and secure lock all release reports and stop
the helper; a mode switch stops the writer before usb-signaller changes the
endpoints beneath it.
## One conversation
Hands is a limb of the existing Face/Souveraine connection, not a compact
second chat client. Opening it resumes the current agent's latest conversation
only when no conversation is attached. Conversation selection and creation
belong to the agent pane, so the Hands header carries no mystery back-arrow or
new-thread button.
The joined surface currently exposes agent text/voice, trackpad, scroll,
left/right click, and a held Ctrl+Alt+Delete. It does not advertise a keyboard
pane merely because an HTML input can emit key reports. A real on-glass
keyboard needs its own QML surface and focus contract before it earns a button.
## The agent's reach
While Hands is joined, the ambient skill points at the substrate's typed
`body` tool: `verb: hand` with `status`, `type`, `key`, `click`, or `pointer`.
It does not teach a second raw `qs` command vocabulary.
A direct request to type ordinary user-authored text is authorization to type
that text, including creative, intimate, or unusual prose. Odd tone is not a
state refusal. The hand still sends nothing Casey did not ask to place on the
attached host, and readiness/state refusals remain visible to the agent.
## Beyond HID
HID can send input and cannot truthfully return a terminal's output. Learned
administration or tunnel payloads therefore belong to a separate, auditable
capability path: named/versioned recipes, an explicit scope and lease, and a
management transport that returns stdout, stderr, and exit status as typed
events. Do not infer success from keystrokes and do not turn an RMM or reverse
tunnel into an opaque keyboard macro.
The NCM control lane can carry that future return path. Its authority,
identity, rollback, and evidence contract must exist before a learned payload
is treated as a skill rather than an unaudited script.
## Proof boundary
Source agreement is only the first line of proof. USB Hands is operational on
a phone only when the signed usb-signaller package contains the persistent
stream helper, the installed Souveraine surface and face assets are the same
revision, and the following motion is observed on glass and on the attached
host:
1. Power Options arms HID while another management path remains reachable.
2. Opening the trackpad starts one helper and receives its `ready` reply.
3. Motion, scroll, left/right click, requested text, a named key, and held
Ctrl+Alt+Delete reach the host without a second writer.
4. A rejected character sends none of that command; leaving and locking leave
no held report.
5. Returning to developer mode restores the control lane cleanly.
KVM adds separate display, storage, NCM, and touch-return acceptance. A green
shell reload, a packaged binary, or the first successful keystroke proves only
its own layer; none of them silently stands in for that cable exercise.

View file

@ -88,6 +88,12 @@ Scope { // Scope
|| GlobalStates.missionPeek) || GlobalStates.missionPeek)
return Dock.DockState.Hidden; return Dock.DockState.Hidden;
// USB Hands owns the bottom of home while its trackpad is actually
// open. Arming the wire alone changes no furniture; opening the
// conditional controller makes the dock yield until it closes.
if (HidController.active)
return Dock.DockState.Hidden;
// A visible keyboard owns the bottom edge. This must precede the home // A visible keyboard owns the bottom edge. This must precede the home
// zone's unconditional PINNED return below; otherwise home stacks the // zone's unconditional PINNED return below; otherwise home stacks the
// dock's exclusive zone under the OSK and charges the display twice. // dock's exclusive zone under the OSK and charges the display twice.

View file

@ -290,12 +290,16 @@ Scope {
SheetLocal.PowerOptionRow { SheetLocal.PowerOptionRow {
Layout.fillWidth: true Layout.fillWidth: true
icon: "keyboard" icon: "keyboard"
title: "USB HID Injection" title: "USB hand"
detail: UsbState.mode === "hid" detail: HidController.active
? "Keyboard and pointer link armed" ? "Wire armed · trackpad open"
: "Let this screen become the other machine's hands" : HidController.modeReady
badge: UsbState.mode === "hid" ? "active" ? "Keyboard and pointer wire armed"
: UsbState.hasMode("hid_mode") ? "wired" : "building" : "Arm the wire, then open its trackpad"
badge: UsbState.busy ? "switching"
: HidController.active ? "open"
: HidController.modeReady ? "armed"
: UsbState.hasMode("hid_mode") ? "ready" : "building"
inset: 10 inset: 10
expandable: true expandable: true
expanded: scope.hidOptionsOpen expanded: scope.hidOptionsOpen
@ -316,35 +320,41 @@ Scope {
SheetLocal.PowerOptionRow { SheetLocal.PowerOptionRow {
Layout.fillWidth: true Layout.fillWidth: true
icon: "cable" icon: "cable"
title: "USB control link" title: HidController.modeReady
detail: UsbState.mode === "hid" ? "Disarm USB hand" : "Arm USB hand"
? "Armed · tap to restore developer USB" detail: HidController.modeReady
? "Restore developer USB and close its controls"
: "Expose keyboard, pointer and developer network" : "Expose keyboard, pointer and developer network"
badge: UsbState.busy ? "switching" badge: UsbState.busy ? "switching"
: UsbState.mode === "hid" ? "active" : HidController.modeReady ? "armed"
: UsbState.hasMode("hid_mode") ? "ready" : "not installed" : UsbState.hasMode("hid_mode") ? "ready" : "not installed"
inset: 28 inset: 28
available: UsbState.hasMode("hid_mode") && !UsbState.busy available: UsbState.hasMode("hid_mode") && !UsbState.busy
onTapped: UsbState.setMode( onTapped: UsbState.setMode(
UsbState.mode === "hid" ? "developer" : "hid") HidController.modeReady ? "developer" : "hid")
} }
SheetLocal.PowerOptionRow { SheetLocal.PowerOptionRow {
Layout.fillWidth: true Layout.fillWidth: true
icon: "keyboard_mouse" icon: "keyboard_mouse"
title: "Agent hands" title: HidController.active
? "Close trackpad" : "Open trackpad"
detail: HidController.active detail: HidController.active
? "Voice, trackpad and the summoned host keyboard" ? "Leave the wire armed; put its controls away"
: "Join the agent to the wire; trackpad first" : "Voice, pointer, scroll and clicks"
badge: HidController.ready ? "awake" badge: HidController.ready ? "awake"
: HidController.active ? "arm" : HidController.active ? "waking"
: GlobalStates.screenLockSecure ? "locked" : "open" : GlobalStates.screenLockSecure ? "locked" : "closed"
inset: 28 inset: 28
available: UsbState.hasMode("hid_mode") available: HidController.modeReady && !UsbState.busy
&& !GlobalStates.screenLockSecure && !GlobalStates.screenLockSecure
onTapped: { onTapped: {
if (Face.joinHands()) if (HidController.active) {
Face.leaveHands();
scope.dismiss(); scope.dismiss();
} else if (Face.joinHands()) {
scope.dismiss();
}
} }
} }
} }

View file

@ -9,9 +9,9 @@
// does by construction. // does by construction.
// //
// USB Hands joins the same limb rather than opening a controller app beside // USB Hands joins the same limb rather than opening a controller app beside
// her. Its agent field, explicit microphone, trackpad and summoned host // her. Its agent field, explicit microphone and trackpad occupy the room the
// keyboard occupy the room the compositor already left below her. The HID // compositor already left below her. The HID reports still belong to
// reports still belong to HidController; this service only routes page intent. // HidController; this service only routes page intent.
// //
// ## Turning her on is joining // ## Turning her on is joining
// //
@ -86,6 +86,12 @@ Singleton {
return true; return true;
} }
function leaveHands() {
if (HidController.active)
HidController.close();
root._syncHands();
}
function leave() { function leave() {
if (root.listening) if (root.listening)
root._talk("cancel"); root._talk("cancel");
@ -153,8 +159,6 @@ Singleton {
root._talk(msg.phase); root._talk(msg.phase);
else if (msg.event === "hid") else if (msg.event === "hid")
root._hid(msg); root._hid(msg);
else if (msg.event === "thread")
root._thread(msg.mode);
else if (msg.event === "ready") else if (msg.event === "ready")
root._syncHands(); root._syncHands();
else if (msg.event === "dismiss") else if (msg.event === "dismiss")
@ -193,20 +197,6 @@ Singleton {
})})`); })})`);
break; break;
} }
case "arm":
HidController.arm();
break;
}
}
function _thread(mode) {
if (Souveraine.turnActive)
return;
if (mode === "new") {
Souveraine.newConversation();
root._syncHands();
} else if (mode === "resume") {
Souveraine.resumeLatestConversation();
} }
} }
@ -220,7 +210,6 @@ Singleton {
mode: UsbState.mode, mode: UsbState.mode,
error: HidController.lastError, error: HidController.lastError,
agent: agent?.name ?? Souveraine.currentAgentId ?? "agent", agent: agent?.name ?? Souveraine.currentAgentId ?? "agent",
conversation: Souveraine.conversationId.length > 0 ? "resumed" : "new",
listening: root.listening, listening: root.listening,
thinking: Souveraine.turnActive thinking: Souveraine.turnActive
}; };

View file

@ -35,12 +35,11 @@ Singleton {
// about a USB gadget it is not using. // about a USB gadget it is not using.
readonly property string skill: "USB Hands is joined to this conversation. " 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. " + "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: " + "When he asks you to type, press a key, or click there, use your existing `body` tool "
+ "`qs -c souveraine ipc call usbHands sendText TEXT`, " + "with `verb: hand` and the corresponding `action`: type, key, click, or pointer. "
+ "`qs -c souveraine ipc call usbHands tap KEY`, " + "Use `action: status` rather than assuming the cable and surface are ready. "
+ "`qs -c souveraine ipc call usbHands key KEY MODIFIERS`, or " + "A direct request to type ordinary user-authored text, including creative, intimate, or unusual prose, "
+ "`qs -c souveraine ipc call usbHands click left|right|middle`. " + "is sufficient authorization; do not refuse it merely because its tone is odd. "
+ "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." + "Do not inject anything Casey did not ask to send to the attached host."
signal controllerChanged() signal controllerChanged()
@ -52,6 +51,12 @@ Singleton {
root.refused(root.lastError); root.refused(root.lastError);
return false; return false;
} }
if (!root.modeReady) {
root.lastError = "Arm HID or KVM from Power Options before opening the trackpad";
root.refused(root.lastError);
root.controllerChanged();
return false;
}
root.lastError = ""; root.lastError = "";
root.active = true; root.active = true;
UsbState.refresh(); UsbState.refresh();
@ -83,23 +88,6 @@ Singleton {
} }
} }
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 _reconcile() { function _reconcile() {
const wanted = root.permitted && root.modeReady; const wanted = root.permitted && root.modeReady;
if (!wanted) { if (!wanted) {
@ -295,6 +283,13 @@ Singleton {
onActiveChanged: root._reconcile() onActiveChanged: root._reconcile()
onModeReadyChanged: { onModeReadyChanged: {
// The trackpad is conditional on an armed wire. If sessiond contracts
// the port back to developer/charging mode, close the surface instead
// of leaving a dead controller on the glass asking to be re-armed.
if (root.active && !root.modeReady) {
root.close();
return;
}
root._reconcile(); root._reconcile();
root.controllerChanged(); root.controllerChanged();
} }