osk: retire the dead dual-keyboard path
This commit is contained in:
parent
dd19ab227f
commit
fb5e9bb80c
4 changed files with 36 additions and 179 deletions
|
|
@ -58,21 +58,14 @@ Scope {
|
|||
// can't flip the wrong way. Known ceiling: squeekboard also shows and
|
||||
// hides ITSELF on input-method focus, and oskOpen doesn't hear about
|
||||
// that — the gesture toggle can need two swipes after an auto-show.
|
||||
// One D-Bus call, no sleep. This used to read
|
||||
// pgrep -x squeekboard >/dev/null || { squeekboard & sleep 1; }
|
||||
// which was correct only while squeekboard was the sole keyboard. Once
|
||||
// stevia became the default (2026-07-21) that pgrep missed on EVERY open:
|
||||
// each one spawned a stray squeekboard to fight stevia for the
|
||||
// sm.puri.OSK0 name, and — the visible damage — slept a full second before
|
||||
// SetVisible. oskOpen flipped instantly, the keyboard arrived ~1s later,
|
||||
// and the gesture rail spent that whole second lifted over nothing.
|
||||
// Ask the bus first; whoever owns the name is the live keyboard. Only if
|
||||
// nobody owns it do we start the sole intended keyboard through the
|
||||
// transition helper; it also re-asserts visibility itself.
|
||||
// Ask the bus first. If no owner exists, start the package-owned service;
|
||||
// the owner monitor below pushes the pending visibility intent as soon as
|
||||
// Squeekboard claims the name.
|
||||
function showOsk() {
|
||||
Quickshell.execDetached(["sh", "-c",
|
||||
"busctl --user call sm.puri.OSK0 /sm/puri/OSK0 sm.puri.OSK0 " +
|
||||
"SetVisible b true 2>/dev/null || osk-switch squeek >/dev/null 2>&1"])
|
||||
"SetVisible b true 2>/dev/null || " +
|
||||
"systemctl --user start squeekboard.service >/dev/null 2>&1"])
|
||||
}
|
||||
function hideOsk() {
|
||||
Quickshell.execDetached(["busctl", "call", "--user",
|
||||
|
|
@ -107,11 +100,8 @@ Scope {
|
|||
|
||||
// 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.
|
||||
// sm.puri.OSK0 is a *name*, not a process. Boot and a crash-restart can
|
||||
// both put a fresh Squeekboard process behind it.
|
||||
// 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
|
||||
|
|
@ -142,14 +132,9 @@ Scope {
|
|||
interval: root.ownerSettleMs
|
||||
onTriggered: {
|
||||
root.ownerSettling = false;
|
||||
// Push once more on the way out. The first push (at adopt time)
|
||||
// can lose a race: osk-switch sleeps 0.3s and then deliberately
|
||||
// SetVisible(true), and a keyboard that self-shows on startup
|
||||
// does so after it has finished coming up — both land *after* we
|
||||
// spoke. Measured 2026-08-05: adopt+hide at 07:37:51, stevia
|
||||
// Visible=true at 07:37:52, leaving oskOpen=false while a
|
||||
// keyboard sat on the screen. Whoever spoke last during the
|
||||
// settle window does not get to be the winner; intent does.
|
||||
// Push once more on the way out. The first push can lose a race to
|
||||
// Squeekboard self-showing after startup. Whoever speaks last
|
||||
// during the settle window does not get to win; intent does.
|
||||
root.assertIntent();
|
||||
}
|
||||
}
|
||||
|
|
@ -254,7 +239,7 @@ Scope {
|
|||
// Unlocking must not leave a keyboard behind.
|
||||
//
|
||||
// Nothing asks for one — the lock module never touches `oskOpen`, and only
|
||||
// polkit takes a hold. squeekboard/stevia self-show whenever input-method
|
||||
// polkit takes a hold. Squeekboard self-shows whenever input-method
|
||||
// focus lands on them (GlobalStates' own note: "self-showed / self-hid
|
||||
// every couple of seconds, ending in Visible=true with no keyboard in front
|
||||
// of the user"), and the surfaces coming back at unlock are exactly such a
|
||||
|
|
|
|||
|
|
@ -12,13 +12,10 @@
|
|||
// by making it wait 350 ms first. Both are gone, along with the double tap's
|
||||
// `hyprctl` fullscreen (START-HERE §3).
|
||||
//
|
||||
// One exception, and it is the keyboard's: with an OSK up, the climb means
|
||||
// only the swap — an upward pull past the swap detent swaps the keyboard
|
||||
// (TASK-38), and the navigation stages (multitasking, home) do not exist for
|
||||
// the duration. The keyboard owns the edge it rides on; a swipe that flips an
|
||||
// app into multitasking while the user was mid-sentence was the complaint as
|
||||
// it presented, and the swap detent has to cost a deliberate reach, not a
|
||||
// near miss. Keyboard down, the two swipe modes are the whole of it.
|
||||
// With an OSK up, upward navigation is gated off. The keyboard owns the edge
|
||||
// it rides on; a swipe that flips an app into multitasking while the user was
|
||||
// mid-sentence was the complaint as it presented. Layout selection now lives
|
||||
// on Squeekboard's own mode key, so the rail has no keyboard-swap gesture.
|
||||
//
|
||||
// The upward gesture is progressive, not a single threshold, and the travel is
|
||||
// *reported* to `ZoneTransition` rather than interpreted here: the compositor
|
||||
|
|
@ -56,17 +53,6 @@ PanelWindow {
|
|||
anchors.bottom: true
|
||||
implicitWidth: 200
|
||||
implicitHeight: Config.options?.dock.gestureRailHeight ?? 32
|
||||
// Last measured squeekboard height; the probe below corrects it every
|
||||
// time the keyboard opens (rotation changes it). 315 = portrait height
|
||||
// on the Pixel's 540x1080 logical screen, the value measured live —
|
||||
// only a first-boot fallback until the first probe lands.
|
||||
// Stevia's portrait height is 200px on the 540x1080 logical screen (the
|
||||
// probe below corrects it live per rotation). Fallback WAS 315 — a stale
|
||||
// squeekboard value that lifted the pill WAY above stevia's top edge,
|
||||
// landing it on the top key row and inside stevia's upward-opening layout
|
||||
// menu ("keyboard selection not working", 2026-07-21). The pill must stay
|
||||
// visible — it is the dismiss handle — just resting exactly at the
|
||||
// keyboard's top edge, never over the keys.
|
||||
// The compositor places this above the keyboard. Nothing here measures it.
|
||||
//
|
||||
// This used to be `ExclusionMode.Ignore` — layer-shell's exclusive zone
|
||||
|
|
@ -82,8 +68,7 @@ PanelWindow {
|
|||
// Zone 0 with Normal is the shell's own idiom for this (SidebarRight,
|
||||
// SidebarLeft unpinned, ReloadPopup): reserve nothing, but respect what
|
||||
// others reserved. The keyboard already declares an exclusive zone, so the
|
||||
// compositor anchors this to the top of it — correct for any keyboard, any
|
||||
// height, any rotation, and across a swap, with no measurement to go stale.
|
||||
// compositor anchors this to its top — correct at any height or rotation.
|
||||
//
|
||||
// START-HERE §3: a control that worked under Hyprland and does nothing now
|
||||
// is almost always a hyprctl or a hyprland.lua binding, and the fix is
|
||||
|
|
@ -100,11 +85,9 @@ PanelWindow {
|
|||
exclusionMode: GlobalStates.oskOpen ? ExclusionMode.Normal : ExclusionMode.Ignore
|
||||
exclusiveZone: 0
|
||||
color: "transparent"
|
||||
// Top, not Overlay: stevia's completion bar / layout menu are child
|
||||
// surfaces of the OSK (which lives on Top). On Overlay the pill sat above
|
||||
// them, so tapping word-completion opened a menu that rendered *behind*
|
||||
// the pill and looked broken. On Top the pill shares the OSK's band and
|
||||
// those popups draw over it. Tradeoff: dock and mission control are on
|
||||
// Top, not Overlay: Squeekboard's layout chooser is a child surface of the
|
||||
// OSK. On Overlay the pill could sit above that chooser and steal its taps;
|
||||
// on Top the popup draws over the pill. Dock and mission control are on
|
||||
// Overlay, so the pill now sits behind them when they're open — fine,
|
||||
// since those are summoned *from* the pill and occupy the screen anyway.
|
||||
// The pill still clears fullscreen apps (below Top), which is its job.
|
||||
|
|
@ -113,11 +96,8 @@ PanelWindow {
|
|||
|
||||
// The height probe that used to live here is gone, along with its retry
|
||||
// timer and `probeOskHeight()`. It existed to answer "how tall is the
|
||||
// keyboard right now", including across a swap where stevia (348px) and
|
||||
// squeekboard (315px) differ and `oskOpen` never flips — and getting that
|
||||
// answer wrong put the pill 33px inside the new keyboard's top rows, where
|
||||
// it reads as simply gone (observed 2026-07-29: rail y=733 against an osk
|
||||
// top edge of 732).
|
||||
// keyboard right now"; getting that answer wrong put the pill inside the
|
||||
// keyboard's top rows, where it read as simply gone.
|
||||
//
|
||||
// The compositor answers it now, for free and always correctly, because the
|
||||
// keyboard's exclusive zone is the same fact the probe was reconstructing.
|
||||
|
|
@ -151,14 +131,6 @@ PanelWindow {
|
|||
// idiom for this (`SelectionHost`, `FullscreenPolkitWindow`).
|
||||
readonly property int missionAt: Math.max(revealAt + 60,
|
||||
Math.round((rail.screen?.height ?? 1080) * 0.18))
|
||||
// TASK-38: one more detent PAST the mission/max stage, armed only while an
|
||||
// OSK is up, that swaps stevia <-> squeekboard. stevia dropped its terminal
|
||||
// layout in 0.56.0-9, so this gesture is the only way to reach a terminal
|
||||
// keyboard. It is the ONLY stage while a keyboard is up — the nav stages
|
||||
// are gated off (see `dragStage`), so the pull is a deliberate, exclusive
|
||||
// reach rather than a near-miss of Mission Control.
|
||||
readonly property int swapAt: 300
|
||||
|
||||
// Speed and length become one number: how far the thumb would have carried
|
||||
// had it kept going. 130 ms is where the old rule sat — "1.1 px/ms promotes
|
||||
// one stage" over a 146 px stage gap — so the feel is the same minus the
|
||||
|
|
@ -280,31 +252,12 @@ PanelWindow {
|
|||
}
|
||||
|
||||
// Stage the current travel has reached: 0 none, 1 reveal, 2 mission.
|
||||
// With a keyboard up the navigation stages do not exist — the climb means
|
||||
// only the swap, and an app mid-sentence has no business shrinking under
|
||||
// the thumb (the drag is the keyboard's then, not the canvas's).
|
||||
// With a keyboard up the navigation stages do not exist; an app
|
||||
// mid-sentence has no business shrinking under the thumb.
|
||||
readonly property int dragStage: GlobalStates.oskOpen ? 0
|
||||
: dragTravel >= missionAt ? 2
|
||||
: dragTravel >= revealAt ? 1 : 0
|
||||
|
||||
// Stage 3, keyboard only. Kept out of dragStage so none of the existing
|
||||
// stage arithmetic changes meaning.
|
||||
readonly property bool swapArmed: GlobalStates.oskOpen && dragTravel >= swapAt
|
||||
// A detent has to be feelable or the gesture can't be found without
|
||||
// looking, which is the whole argument for having it. Fires once on the
|
||||
// crossing, not per frame.
|
||||
onSwapArmedChanged: if (rail.swapArmed) Haptics.tick()
|
||||
|
||||
// The swap stops one keyboard unit and starts another; in that gap the osk
|
||||
// layer unmaps and margins.bottom would collapse to 0 and snap back, which
|
||||
// reads as the pill falling off the screen. Hold the lift across the gap.
|
||||
property bool swapping: false
|
||||
Timer {
|
||||
id: swapSettle
|
||||
interval: 4000
|
||||
onTriggered: rail.swapping = false
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: handle
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
|
@ -319,17 +272,12 @@ PanelWindow {
|
|||
: rail.dragStage >= 1 ? 170 : 150)
|
||||
+ Math.min(rail.dragTravel * 0.08, 24)
|
||||
height: 7 + (rail.dragStage >= 2 ? 3 : rail.dragStage >= 1 ? 1 : 0)
|
||||
// Square off at the swap detent. Stage 2 is already the widest and
|
||||
// brightest the pill gets, so the extra step needs a change of SHAPE to
|
||||
// read as a further stage rather than more of the same.
|
||||
radius: rail.swapArmed ? 2 : height / 2
|
||||
radius: height / 2
|
||||
|
||||
// Brighten and tint toward the accent as the drag escalates. Stage 2
|
||||
// pulls the handle to the theme accent — the "you've reached Mission
|
||||
// Control" tell.
|
||||
color: rail.swapArmed
|
||||
? (Appearance?.colors?.colSecondary ?? "#ffd7a0")
|
||||
: rail.dragStage >= 2
|
||||
color: rail.dragStage >= 2
|
||||
? (Appearance?.colors?.colPrimary ?? "#a0c8ff")
|
||||
: "#e6ffffff"
|
||||
// Invisible while the dock is up and nothing is being dragged — the dock
|
||||
|
|
@ -505,11 +453,9 @@ PanelWindow {
|
|||
// the drag is abandoned there.
|
||||
//
|
||||
// The pose is a navigation gesture's transition. With a keyboard
|
||||
// up the climb is the swap pull instead — a scale that starts while
|
||||
// the finger is still short of the swap detent is the app "going
|
||||
// into multitasking" for a gesture that is not about multitasking,
|
||||
// which is exactly the wrong read (observed 2026-08-06). The
|
||||
// keyboard's climb touches nothing; only the swap commits.
|
||||
// up the climb belongs to the keyboard instead. A scale under that
|
||||
// thumb reads as the app entering multitasking, so the keyboard's
|
||||
// climb deliberately touches nothing.
|
||||
if (!GlobalStates.oskOpen) {
|
||||
// Name the destination once, at the moment this becomes an
|
||||
// upward pull — quickstep's `getSwipeUpDestinationAndLength`
|
||||
|
|
@ -526,10 +472,8 @@ PanelWindow {
|
|||
// running long before anything could commit and the motion is
|
||||
// still continuous from the start of the climb.
|
||||
//
|
||||
// Not with a keyboard up: there the climb is the swap pull and
|
||||
// touches nothing. An app shrinking under a thumb that was
|
||||
// reaching for a different keyboard is the wrong read, and was
|
||||
// the keyboard-swap complaint as it presented (2026-08-06).
|
||||
// Not with a keyboard up: the climb touches nothing. Layout
|
||||
// choice is an explicit key on Squeekboard itself.
|
||||
if (!ZoneTransition.inFlight && rail.dragTravel >= 8)
|
||||
ZoneTransition.begin()
|
||||
// The clock, and nothing derived from it. `pullTo` turns this
|
||||
|
|
@ -579,38 +523,13 @@ PanelWindow {
|
|||
const projected = travel + speed * rail.projectMs
|
||||
rail.dragTravel = 0
|
||||
|
||||
// TASK-38: past the swap detent with a keyboard up, this gesture IS
|
||||
// the keyboard swap — it does not also commit Mission Control on
|
||||
// the way through. No flick promotion either: the swap costs a
|
||||
// deliberate pull, and this way the commit matches exactly what
|
||||
// swapArmed lit up under the thumb.
|
||||
//
|
||||
// Routed through the gesture table rather than calling osk-switch
|
||||
// here, per INTERFACE-ARCHITECTURE §4 — the binding stays data, so
|
||||
// it can be re-pointed without editing this file.
|
||||
if (GlobalStates.oskOpen && travel >= rail.swapAt) {
|
||||
rail.swapping = true
|
||||
swapSettle.restart()
|
||||
// No re-probe across the swap. The incoming keyboard is a
|
||||
// different height and that used to strand the pill inside the
|
||||
// new one's keys; the compositor re-anchors this surface when
|
||||
// the new exclusive zone lands, so the height nobody measures
|
||||
// is the height that cannot be wrong.
|
||||
Gestures.deliver("osk-swap", Gestures.oskSwapAction)
|
||||
rail.dragging = false
|
||||
return
|
||||
}
|
||||
|
||||
// Upward: pick the committed stage from travel, then let a fast
|
||||
// flick promote it one step (a quick short throw still reaches
|
||||
// Mission Control).
|
||||
//
|
||||
// Navigation is OFF while the keyboard is up: the climb means only
|
||||
// the swap, and a pull that fell short of the swap detent is an
|
||||
// abandoned pull, not a mission-to-home gesture. A short swipe that
|
||||
// flipped the app into multitasking was the keyboard-swap complaint
|
||||
// as it actually presented (observed 2026-08-06) — the swap path
|
||||
// above is the *only* thing a keyboard-up climb can commit.
|
||||
// Navigation is OFF while the keyboard is up: the climb is an
|
||||
// abandoned pull, not a mission-to-home gesture. Layout selection
|
||||
// stays on Squeekboard's mode key.
|
||||
// `travel` floors it: projection alone would turn a 10 px smudge
|
||||
// flicked in 5 ms into a 270 px throw, i.e. Home. Bare `tapSlop` —
|
||||
// it is the MouseArea's. The old flick rule read `rail.tapSlop`,
|
||||
|
|
@ -663,9 +582,8 @@ PanelWindow {
|
|||
// competing for the one region a thumb rests in — and one of them
|
||||
// could only resolve by making the other wait 350 ms first.
|
||||
//
|
||||
// Not while the keyboard is up: a short climb there is a swap pull
|
||||
// that fell short — a failed attempt at a gesture that exists —
|
||||
// not a failed attempt at one that does not.
|
||||
// Not while the keyboard is up: navigation discovery should not
|
||||
// train against a gesture that is intentionally gated off.
|
||||
if (travel > tapSlop && !GlobalStates.oskOpen)
|
||||
maybeNudgeDiscovery()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue