Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/INTERFACE-ARCHITECTURE.md

9.5 KiB

Phone shell — interface architecture

The phone has four primary interfaces the user actually touches: the pill, the dock, the keyboard, and touch/gesture. This doc names what each one is meant to be, what it is today, and the single seam where a hardcode should become a small framework so the surface can grow without rewrites.

Principle: clean beats feature-rich. Each surface has one job and one place its behaviour is tuned. A new feature should be a new entry in an existing table, not a new hardcoded branch. Where a value is tuned in more than one place today, that is the bug — not the value.

Truth hierarchy: this doc describes intent; HOW-IT-WORKS.md describes the current wiring; the code is the reality. When they disagree, fix the code or fix the doc in the same change — never leave a third story.


1. Pill — the always-on gesture bar

Meant to be: the one surface that is always reachable, on any app, in any state. A thin, dumb input strip at the bottom edge that recognises a small fixed vocabulary of gestures and forwards them — it owns no app state and makes no decisions beyond "which gesture was this."

Today (pill/shell.qml): WlrLayer.Overlay, ExclusionMode.Ignore, margins.bottom:0. Gesture arbitration is one MouseArea (double-tap, swipe up, swipe down) → dock IPC. This part is clean and well-reasoned.

The seam / the bug: the pill's z-order guarantee is documented but not enforced. pill/shell.qml:23-27 sets namespace quickshell:pill and points at "the order layerrule in hyprland.lua" — that layerrule does not exist (hyprland.lua only has a commented-out generic ^my-overlay$ example). So after an ii restart the pill can be buried under the dock and stop taking touch — the exact failure the comment says is solved. Fix: add the real order layerrule for quickshell:pill, or drop the promise from the code and docs. Do not add more pill gestures until stacking is enforced — an always-on surface that silently dies is worse than a plain one.

Growth rule: new pill gestures are new cases in the one MouseArea, each forwarding to a named IPC target — never doing app logic inline.


2. Dock — the app switcher and stack surface

Meant to be: the surface that shows and switches running/pinned apps and lets the user group them into stacks. It owns app-presentation state; it does not own gesture recognition (the pill and hyprgrass feed it via IPC).

Today: interaction layer (Dock.qml, DockAppButton.qml, DockStack.qml) is strong — drag-to-combine, fan-out arc, visibility state machine (computeDockState). The weak layer is state/config:

  • Stacks are a hand-parsed string "stackId|appId,appId" (TaskbarApps.qml:25). A | or , in a name corrupts it silently; parsed/encoded in ~6 places. → move to one JSON blob per entry (array-of-strings the adapter tolerates, but each string is JSON): kills the delimiter-corruption class, keeps the adapter happy. This is the agreed direction.
  • Stack ids are count-based (nextStackName = "Stack " + (count+1), :87) → collide after a delete, and id is the lookup key. Two stacks can share an id and cross-contaminate members. → stable uid, display name separate from id.
  • Combining into a new stack suppresses the target's standalone pin at render time (apps getter) but never removes it from pinnedApps in config (:99-108) → the pin lingers; unstack works by luck. → remove-on-combine so config matches what's shown.
  • Tap-cycle lastFocused index (DockAppButton :196) is never reconciled when windows open/close and is also written by hover (:180) → tapping can focus the wrong window. → clamp/reconcile against the live toplevel model.
  • Magic numbers live in the components: pillStripHeight 32 (Dock.qml:161), dwell 500 (twice: DockAppButton:133, DockStack:116), double-tap 350, arc geometry (DockStack:33-36). → lift into Config.options.dock next to the height/monochromeIcons/hoverRegionHeight that already live there.
  • reorderStackMember (TaskbarApps:113) is dead — superseded by setStackOrder. → remove, or wire the menu reorder it was meant for.

The seam: Config.options.dock already exists — the framework anchor is already there. "More structure" here means finishing that namespace (all tuning knobs + a real stack schema under it), not inventing a new one.

Growth rule: dock feel/config knobs go under Config.options.dock; stack members are structured entries, not delimited strings. Rename + ungroup are table-stakes and should exist before any fancier stack feature.


3. Keyboard (OSK) — squeekboard

Meant to be: an on-screen keyboard that appears on text focus and can be summoned by one deliberate gesture, driven through one IPC target so its visibility and any focus-grab shield always move together.

Today: squeekboard, launched from hyprland.lua, auto-shows via input-method-v2, toggled by 3-finger swipe-up routed through ii's osk IPC. The routing is right (single osk target).

The seam / the drift: the comments and the retained quickshell-ii-patches/OnScreenKeyboard.qml still describe wvkbd (retired 2026-07-10) and its focus-grab shield — the osk IPC now drives squeekboard, but the code comments and the history-only patch file still tell the wvkbd story. Same doc-vs-reality drift as the pill layerrule. → update the comments to squeekboard reality; confirm whether OnScreenKeyboard.qml's GlobalStates.oskOpen plumbing is still the thing osk toggle hits, or vestigial.

Growth rule: one osk IPC target owns show/hide; layout/theme is squeekboard config, addressable later from souveraine-settings. No second path to toggle the keyboard.


4. Touch / gesture — the input spine

Meant to be: one routing layer that maps a physical touch gesture to a named interface action. Today this is the messiest surface because it is spread across four dispatch styles.

Today (hyprland.lua):

  • hl.gesture{fingers=3,horizontal} → workspace (Hyprland-native)
  • hl.plugin.hyprgrass.bind{...} → close / terminal / edge-swipes → qs -c ii ipc call <target>
  • the pill's own MouseArea → dock IPC
  • touchdevice.output = "DSI-1" binds touch to the panel for rotation

Two input sources (pill swipe-up, hyprgrass edge-swipe-up) both drive the dock; no single arbiter. Gesture → action bindings are inline exec_cmd strings scattered through the file.

The seam: this is the memory note "don't hardcode bindings blindly; need a routing system to avoid overlap." The clean form is one gesture table — edge/finger/direction → named IPC target — read top-to-bottom so overlaps are visible in one place. Every binding already ends in a qs ... ipc call <target>; the framework is just making that table explicit and single-sourced instead of sprinkled through hyprland.lua.

Growth rule: a new gesture is a new row in the gesture table pointing at a named IPC target. Two rows may not target the same action from different edges without being adjacent in the table (so overlap is obvious).


Cross-cutting: the one recurring defect

Three of four surfaces have the same flaw — a documented guarantee the code doesn't enforce: the pill layerrule (doc says pinned, code doesn't pin), the keyboard (comments say wvkbd, code runs squeekboard), the dock stack pin (render says suppressed, config says still pinned). Cleaning these up is mostly making the code match its own stated contract, then giving each surface exactly one place it's tuned.

Status (2026-07-13)

  1. Pill layerrule DONE — pill-above-dock rule added to hyprland.lua (order = 1 on ^quickshell:pill$). VERIFY ON GLASS: order passthrough in the Lua layer_rule wrapper is untested on this build.
  2. Gesture table DONE — hyprgrass binds are one touch_gestures table in hyprland.lua; the two other gesture sources (hl.gesture workspace swipe, the pill's MouseArea) are listed in its header so overlap stays visible in one read.
  3. Dock state layer DONE — JSON-per-entry stacks (legacy pipe entries still parse, rewritten on next write), minted stack-N ids that never reuse after delete, name separate from id (+ renameStack(), no UI yet), pin actually removed from config on combine, tap-cycle starts from the truly focused window, dead reorderStackMember removed, dragDwellMs/pillStripHeight lifted into Config.options.dock.
  4. Keyboard DONE — hyprland.lua comments tell the squeekboard story; OnScreenKeyboard.qml (the "osk" IPC owner) moved into this surface + deploy manifest. Remaining hand-deployed patches (BarContent, Network, QuickSliders, switchwall.sh) noted in overlays/QUICKSHELL-MOVED.md.

Open decision — two-stage swipe-up was removed, not lost by accident?

Souveraine commit 6e21c50 ("resume agent conversations from server") reworked the dock state machine: the Peek state, dockRevealPulse, and the two-stage swipeUp (reveal → escalate to overview) all went away in favor of a flat reveal/hide contract — under a commit title that says nothing about the dock. Pixel3Arch CLAUDE.md still records two-stage as SHIPPED (2026-07-11). The replacement comment is articulate, so this reads as a deliberate redesign, but it needs Casey's confirmation: keep the flat contract (overview = the dock's apps button), or restore the escalation. Comments in hyprland.lua/HOW-IT-WORKS now describe the flat behavior that actually runs.