Watch
1
0
Fork
You've already forked souveraine
0

shell: an owner change is not a visibility event

sm.puri.OSK0 is a name, not a process. Boot, a crash-restart and an
osk-switch swap each put a fresh keyboard behind it, and the visibility
monitor mirrored the newcomer's opening claim into oskOpen as though the
user had asked for a keyboard. Measured: shell up 03:03:38, self-showed
at 03:03:43, nobody near the phone.

Track the bus owner - gdbus already prints it and the 'Visible' filter
was throwing it away - and on a change push oskOpen at the new owner
instead of pulling state out of it. A process that just started has no
history; the shell is the continuity of intent across the keyboard's
lifetime.
This commit is contained in:
Fimeg 2026-08-05 07:34:53 -04:00
commit d7a04f27d5

View file

@ -105,6 +105,63 @@ Scope {
property int maxReasserts: 5
property int reassertCount: 0
// An owner change is not a visibility event.
//
// sm.puri.OSK0 is a *name*, not a process. Three different things put a
// fresh process behind it: boot, a crash-restart (the keyboards exit
// `Error reading events from display: Broken pipe` when their Wayland
// connection is perturbed, and both units are Restart=on-failure), and
// `osk-switch` swapping squeekboard for stevia at the user's gesture.
// Each new process announces its own idea of Visible, and until now this
// monitor mirrored that into oskOpen indistinguishable from the
// keyboard deciding to show itself. Measured on 2026-08-05: shell up at
// 03:03:38, `[osk] squeekboard self-showed` at 03:03:43, nine seconds
// after boot with nobody near the phone.
//
// A process that just started has no history. It cannot know whether the
// user wanted a keyboard. The shell does oskOpen is the continuity of
// that intent across the keyboard's whole lifetime. So on an owner change
// we PUSH intent onto the new owner instead of PULLING state out of it,
// and we ignore its opening claim while our push is in flight.
//
// This is also why "the keyboard comes back on wake" needs no compositor
// change to stop hurting: wake perturbs the display connection, the OSK
// restarts, and it is the mirror not the compositor and not the
// keyboard that turns that restart into a keyboard on the user's
// screen.
property string oskOwner: ""
property int ownerSettleMs: 2000
// NOT a binding on Date.now() QML bindings do not re-evaluate because
// time passed, so a `Date.now() - t < ms` property latches at creation
// and never clears. A timer is the only honest way to express "for a
// moment after".
property bool ownerSettling: false
Timer {
id: ownerSettleTimer
interval: root.ownerSettleMs
onTriggered: root.ownerSettling = false
}
function adoptOwner(owner: string): void {
if (owner === root.oskOwner) return;
const previous = root.oskOwner;
root.oskOwner = owner;
root.ownerSettling = true;
ownerSettleTimer.restart();
if (owner === "") {
console.log("[osk] keyboard left the bus (was " + previous + ")");
return;
}
console.log("[osk] keyboard is now " + owner
+ (previous === "" ? "" : " (was " + previous + ")")
+ "; re-asserting oskOpen=" + GlobalStates.oskOpen);
// Push our intent, not theirs. A fresh keyboard claiming Visible=true
// when nobody asked for one gets closed here.
if (GlobalStates.oskOpen) root.showOsk();
else root.hideOsk();
}
Connections {
target: GlobalStates
function onOskHoldsChanged() {
@ -119,8 +176,34 @@ Scope {
"--dest", "sm.puri.OSK0", "--object-path", "/sm/puri/OSK0"]
stdout: SplitParser {
onRead: line => {
// gdbus prints the owner of --dest at startup and again on
// every NameOwnerChanged. These lines were being dropped by
// the 'Visible' filter below; they are the evidence we need.
// The name sm.puri.OSK0 is owned by :1.41
// The name sm.puri.OSK0 does not have an owner
const owned = line.match(/is owned by (\S+)/);
if (owned) {
root.adoptOwner(owned[1]);
return;
}
if (line.includes("does not have an owner")) {
root.adoptOwner("");
return;
}
if (!line.includes("'Visible'")) return;
const vis = line.includes("<true>");
// Still inside an owner change: this is the new process
// introducing itself, or the echo of the intent we just
// pushed at it. Either way it is not the user speaking.
if (root.ownerSettling) {
if (vis !== GlobalStates.oskOpen)
console.log("[osk] ignoring Visible=" + vis
+ " from freshly-arrived " + root.oskOwner
+ "; oskOpen=" + GlobalStates.oskOpen + " stands");
return;
}
if (!vis && GlobalStates.oskHolds.length > 0) {
if (root.reassertCount >= root.maxReasserts) {
console.log("[osk] squeekboard self-hid under hold ["