hands: arm the wire before opening the pad
This commit is contained in:
parent
799f0650ed
commit
59b52a818b
7 changed files with 210 additions and 148 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
122
saf/surface/03-usb-hands.md
Normal 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.
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue