Watch
1
0
Fork
You've already forked souveraine
0

osk: polkit holds the keyboard open instead of poking it once

squeekboard hides itself when input-method focus drops, so the password
field was left with no keyboard. A hold re-asserts through that and
releases on dismiss; a manual close drops the hold.
This commit is contained in:
Fimeg 2026-07-26 16:56:34 -04:00
commit 7475fae505
3 changed files with 120 additions and 9 deletions

View file

@ -47,6 +47,47 @@ Singleton {
property bool osdVolumeOpen: false
// WRITER: ii OSK module (OnScreenKeyboard.qml)
property bool oskOpen: false
// WRITER: OnScreenKeyboard.qml (via oskHold/oskRelease/oskDropHolds).
// The names of surfaces that need the keyboard for as long as they are
// up, rather than for the instant they appeared.
//
// Setting oskOpen once is not enough. squeekboard hides ITSELF whenever
// input-method focus drops (the journal is full of `self-hid`), and a
// layer-shell password prompt does not reliably hold that focus so a
// surface that pokes the keyboard open gets a keyboard that vanishes
// while the field is still waiting for input. A hold survives that: the
// keyboard is re-asserted for as long as the holder is up.
//
// A user close outranks every hold (oskDropHolds). The person in front
// of the phone gets to dismiss the keyboard even mid-prompt.
property var oskHolds: []
// oskOpen as it stood when the first hold was taken, so releasing the
// last hold restores rather than clobbers a keyboard the user had open.
property bool oskHoldRestoreOpen: false
function oskHold(reason) {
if (root.oskHolds.indexOf(reason) >= 0) return;
if (root.oskHolds.length === 0)
root.oskHoldRestoreOpen = root.oskOpen;
root.oskHolds = root.oskHolds.concat([reason]);
root.oskOpen = true;
}
function oskRelease(reason) {
const next = root.oskHolds.filter(r => r !== reason);
if (next.length === root.oskHolds.length) return;
root.oskHolds = next;
if (next.length === 0)
root.oskOpen = root.oskHoldRestoreOpen;
}
// The user closed the keyboard by hand. Every hold drops otherwise the
// re-assert below would fight them key for key.
function oskDropHolds() {
if (root.oskHolds.length === 0) return;
root.oskHolds = [];
root.oskHoldRestoreOpen = false;
}
// WRITER: ii overlay module
property bool overlayOpen: false
// WRITER: ii overview module + Dock.qml IPC

View file

@ -86,6 +86,21 @@ Scope {
// squeekboard's real Visible property back into oskOpen so there is
// exactly one truth. Setting oskOpen from here re-triggers SetVisible
// with the value squeekboard already has, which is a harmless no-op.
// A self-hide is squeekboard's opinion, not the user's. While a surface
// holds the keyboard (GlobalStates.oskHolds polkit's password field is
// the first) that opinion is overridden and the keyboard comes straight
// back. Bounded: after this many re-asserts within one hold we stop and
// say so, rather than trading SetVisible calls with squeekboard forever.
property int maxReasserts: 5
property int reassertCount: 0
Connections {
target: GlobalStates
function onOskHoldsChanged() {
if (GlobalStates.oskHolds.length > 0) root.reassertCount = 0;
}
}
Process {
id: oskVisMonitor
running: true
@ -95,6 +110,22 @@ Scope {
onRead: line => {
if (!line.includes("'Visible'")) return;
const vis = line.includes("<true>");
if (!vis && GlobalStates.oskHolds.length > 0) {
if (root.reassertCount >= root.maxReasserts) {
console.log("[osk] squeekboard self-hid under hold ["
+ GlobalStates.oskHolds.join(",") + "] more than "
+ root.maxReasserts + " times; giving up the hold");
GlobalStates.oskDropHolds();
GlobalStates.oskOpen = false;
return;
}
root.reassertCount++;
console.log("[osk] squeekboard self-hid while held by ["
+ GlobalStates.oskHolds.join(",") + "]; re-asserting ("
+ root.reassertCount + "/" + root.maxReasserts + ")");
root.showOsk();
return;
}
if (GlobalStates.oskOpen !== vis) {
console.log("[osk] squeekboard self-" + (vis ? "showed" : "hid") + ", syncing oskOpen");
GlobalStates.oskOpen = vis;
@ -103,21 +134,35 @@ Scope {
}
}
// Every deliberate close pill, gesture, shortcut, IPC goes through
// here, because closing by hand is what drops a hold.
function userClose() {
GlobalStates.oskDropHolds();
GlobalStates.oskOpen = false;
}
IpcHandler {
target: "osk"
function toggle(): void {
GlobalStates.oskOpen = !GlobalStates.oskOpen;
if (GlobalStates.oskOpen) root.userClose();
else GlobalStates.oskOpen = true;
}
function close(): void {
GlobalStates.oskOpen = false;
root.userClose();
}
function open(): void {
GlobalStates.oskOpen = true;
}
// Which surfaces are holding the keyboard open, if any. The pill and
// the agent both ask "why won't this close"; this answers it.
function holds(): string {
return JSON.stringify(GlobalStates.oskHolds);
}
// Double-tapping the pill while the keyboard is open calls this
// pops the (suppressed, see Dock.qml) dock back up for a few
// seconds without having to close the keyboard first.
@ -131,7 +176,8 @@ Scope {
description: "Toggles on screen keyboard on press"
onPressed: {
GlobalStates.oskOpen = !GlobalStates.oskOpen;
if (GlobalStates.oskOpen) root.userClose();
else GlobalStates.oskOpen = true;
}
}
@ -149,7 +195,7 @@ Scope {
description: "Closes on screen keyboard on press"
onPressed: {
GlobalStates.oskOpen = false;
root.userClose();
}
}
}

View file

@ -10,20 +10,44 @@ import Quickshell.Wayland
// Souveraine: on a phone deploy, surface the on-screen keyboard when a polkit
// prompt appears. The polkit window is a wlr-layer-shell Overlay and the OSK
// is a separate layer-shell surface (squeekboard), so the OSK won't auto-rise
// to it. Since both run under this shell, we drive the OSK explicitly: open
// it when polkit activates, close it when it dismisses. Laptop deploys
// (souveraine.phone == false) are unchanged hardware keyboard handles it.
// to it. Since both run under this shell, we drive the OSK explicitly.
// Laptop deploys (souveraine.phone == false) are unchanged hardware
// keyboard handles it.
//
// This was a poke `oskOpen = PolkitService.active` on the transition and
// a poke is not enough for two reasons, both of which left a password field
// on screen with no way to type into it:
//
// 1. squeekboard hides itself when input-method focus drops, which this
// layer-shell surface does not reliably hold. The keyboard appeared and
// then left while the prompt was still waiting.
// 2. The transition is all a poke sees. pkexec run before Config.ready
// an update on login, a boot-time authorization has its prompt up
// before this file is even loaded, so no transition ever arrives.
//
// A hold fixes both: the keyboard is re-asserted for as long as the prompt
// is up (GlobalStates.oskHold), and onCompleted takes the hold for a prompt
// that was already waiting.
FullscreenPolkitWindow {
id: root
contentComponent: Component {
PolkitContent {}
}
readonly property bool phone: Config.options?.souveraine?.phone ?? false
function syncKeyboard() {
if (!root.phone) return;
if (PolkitService.active) GlobalStates.oskHold("polkit");
else GlobalStates.oskRelease("polkit");
}
Component.onCompleted: root.syncKeyboard()
Connections {
target: PolkitService
function onActiveChanged() {
if (!Config.options.souveraine || !Config.options.souveraine.phone) return;
GlobalStates.oskOpen = PolkitService.active;
root.syncKeyboard();
}
}
}