Watch
1
0
Fork
You've already forked souveraine
0

shell: recover the phone-only edits into the repo

~/souveraine-surfaces/quickshell on the phone is not a git repo and was the
only copy of five files. Brought back verbatim:

- Gestures.qml, SystemGestureRail.qml: TASK-38 osk-swap detent (DUMP §5, §6)
- OnScreenKeyboard.qml: showOsk asks the bus instead of pgrep+sleep 1
- DockAppButton.qml, DockStack.qml: suffix-tolerant AppSearch.resolveEntry

resolveEntry itself had been added to the phone's live ~/.config/quickshell/ii
tree, which deploy.sh rsyncs from ii-base — the next deploy would have deleted
it and left the two dock callers referring to nothing. It lands in ii-base here.
This commit is contained in:
Fimeg 2026-07-29 08:27:27 -04:00
commit 5965c64781
6 changed files with 184 additions and 17 deletions

View file

@ -21,7 +21,10 @@ DockButton {
property bool appIsActive: appToplevel.toplevels.find(t => (t.activated == true)) !== undefined
readonly property bool isSeparator: appToplevel.appId === "SEPARATOR"
property var desktopEntry: DesktopEntries.heuristicLookup(appToplevel.appId)
// Suffix-tolerant resolve: a bare heuristicLookup returns null for app_ids
// that carry an instance suffix (Firefox reports "firefox-default"), and a
// null entry makes tapping a pinned-but-not-running icon a silent no-op.
property var desktopEntry: AppSearch.resolveEntry(appToplevel.appId)
enabled: !isSeparator
implicitWidth: isSeparator ? 1 : implicitHeight - topInset - bottomInset
@ -29,7 +32,7 @@ DockButton {
target: DesktopEntries
function onApplicationsChanged() {
root.desktopEntry = DesktopEntries.heuristicLookup(appToplevel.appId);
root.desktopEntry = AppSearch.resolveEntry(appToplevel.appId);
}
}

View file

@ -85,7 +85,8 @@ DockButton {
running.activate();
return;
}
const entry = DesktopEntries.heuristicLookup(members[i]);
// Suffix-tolerant — see DockAppButton.desktopEntry.
const entry = AppSearch.resolveEntry(members[i]);
entry?.execute();
}

View file

@ -58,10 +58,21 @@ 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 one, and osk-switch picks the right unit and
// re-asserts visibility itself.
function showOsk() {
Quickshell.execDetached(["sh", "-c",
"pgrep -x squeekboard >/dev/null || { squeekboard & sleep 1; }; " +
"busctl call --user sm.puri.OSK0 /sm/puri/OSK0 sm.puri.OSK0 SetVisible b true"])
"busctl --user call sm.puri.OSK0 /sm/puri/OSK0 sm.puri.OSK0 " +
"SetVisible b true 2>/dev/null || osk-switch stevia >/dev/null 2>&1"])
}
function hideOsk() {
Quickshell.execDetached(["busctl", "call", "--user",

View file

@ -34,6 +34,7 @@ import Quickshell.Hyprland
import Quickshell.Wayland
import qs
import qs.modules.common
import qs.services
PanelWindow {
id: rail
@ -53,7 +54,7 @@ PanelWindow {
// visible — it is the dismiss handle — just resting exactly at the
// keyboard's top edge, never over the keys.
property int oskLift: 200
margins.bottom: GlobalStates.oskOpen ? rail.oskLift : 0
margins.bottom: (GlobalStates.oskOpen || rail.swapping) ? rail.oskLift : 0
exclusionMode: ExclusionMode.Ignore
color: "transparent"
// Top, not Overlay: stevia's completion bar / layout menu are child
@ -71,13 +72,30 @@ PanelWindow {
// to ~1s after SetVisible on a cold start (OnScreenKeyboard.qml sleeps 1
// before the D-Bus call), so a miss retries once.
property int oskProbeAttempts: 0
property int oskProbeMax: 2
// Re-measure and re-place the pill.
//
// This used to be triggered by oskOpen going true and nothing else, which
// is not enough once the keyboard can be SWAPPED: stevia is 348px tall and
// squeekboard is 315px, and oskOpen does not flip across a swap — the
// keyboard stays "open" the whole time, only the daemon changes. So the
// lift kept the old keyboard's height and the pill landed 33px inside the
// new one's top rows, behind the keys, where it reads as simply gone.
// (Observed 2026-07-29: rail y=733 against an osk top edge of 732.)
// Any path that can change the keyboard's height must call this.
function probeOskHeight(): void {
rail.oskProbeAttempts = 0;
oskProbeRetry.stop();
oskProbe.running = true;
}
Connections {
target: GlobalStates
function onOskOpenChanged() {
if (GlobalStates.oskOpen) {
rail.oskProbeAttempts = 0;
oskProbeRetry.stop();
oskProbe.running = true;
rail.oskProbeMax = 2;
rail.probeOskHeight();
}
}
}
@ -103,14 +121,18 @@ PanelWindow {
}
// Keyboard not mapped yet (cold start takes ~1s). Retry a
// couple of times, then keep the last known height.
if (GlobalStates.oskOpen && rail.oskProbeAttempts < 2)
if (GlobalStates.oskOpen && rail.oskProbeAttempts < rail.oskProbeMax)
oskProbeRetry.restart();
}
}
}
Timer {
id: oskProbeRetry
interval: 1200
// Cold start is slow, but a swap has a real gap where NO osk layer is
// mapped at all (one unit stopped, the next not up yet), so poll harder
// while swapping — otherwise the pill wears the old height for over a
// second after every swap.
interval: rail.swapping ? 400 : 1200
onTriggered: {
rail.oskProbeAttempts += 1;
if (GlobalStates.oskOpen) oskProbe.running = true;
@ -123,6 +145,13 @@ PanelWindow {
// Control; releasing between the two commits the lower stage.
readonly property int revealAt: 48
readonly property int missionAt: 190
// 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. Deliberately beyond missionAt: a swap mid-sentence is
// disruptive, so it has to be an obviously further pull, not a near-miss of
// Mission Control.
readonly property int swapAt: 300
// Live upward travel of the in-flight drag (0 while idle, grows as the
// finger climbs). Drives the handle's appearance so the pill tracks the
@ -134,6 +163,24 @@ PanelWindow {
readonly property int dragStage: 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
@ -148,14 +195,19 @@ PanelWindow {
: rail.dragStage >= 1 ? 170 : 150)
+ Math.min(rail.dragTravel * 0.08, 24)
height: 7 + (rail.dragStage >= 2 ? 3 : rail.dragStage >= 1 ? 1 : 0)
radius: height / 2
// 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
// 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.dragStage >= 2
? (Appearance?.colors?.colPrimary ?? "#a0c8ff")
: "#e6ffffff"
color: rail.swapArmed
? (Appearance?.colors?.colSecondary ?? "#ffd7a0")
: rail.dragStage >= 2
? (Appearance?.colors?.colPrimary ?? "#a0c8ff")
: "#e6ffffff"
opacity: rail.dragging ? 1 : 0.9
// Idle nudge for the discovery hint: a brief scale pulse.
@ -261,6 +313,29 @@ PanelWindow {
const speed = travel / elapsed // px/ms, upward only
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) {
singleTapTimer.stop()
lastTapAt = -1
rail.swapping = true
swapSettle.restart()
// The incoming keyboard is a different height. Keep probing
// across the whole stop/start gap or the pill keeps the old
// one's lift and disappears into the new one's keys.
rail.oskProbeMax = 12
rail.probeOskHeight()
Gestures.deliver("osk-swap", Gestures.oskSwapAction)
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).