Watch
1
0
Fork
You've already forked souveraine
0

publish: the public projection begins here

This is a projection, not a development branch. The tree above was constructed
from the internal source named below under a manifest that decides which paths
may leave, then scanned as a whole tree rather than as a series of patches, and
only then published.

Public history starts here because the history before it was not admissible,
and neither was the tree. What used to stand in this repository included a
rescue copy of another machine, a directory of phone handoffs, deployment
wired to one house, and a submodule pointing at a forge no stranger can reach.
None of that was ever the product. It stays in the private forge, which is
allowed to hold the whole working organism, and this is what was deliberately
sent out instead.

Three mechanisms produced this tree, in decreasing order of trust. A top-level
path the manifest does not name never arrives at all, which is the one that
catches directories nobody has thought of yet. Named internal files inside
admitted roots are dropped. A short, reviewed table replaces deployment
defaults that a public build must not carry -- an endpoint aimed at one LAN, a
VPN profile belonging to one phone, packaging built from one checkout path.

Everything after this commit is an ordinary publication with the same three
trailers, so a force push stops being routine and starts meaning that
something deliberate happened. The trailers bind the projection to its source
without pretending the public SHA is the private one: same lineage, different
tree, and the record says so.

Source-Sha: 8f27b1e76a8fef560a336aba18e6990713ff1047
Policy-Sha: 6b261d2f3e6e1fb19874846ba4bb1dfe15565d25b8618c1c1afba0419c101d27
Tree-Digest: 18ec3563c5e5ef9a414993a9f6734b251ff9ed3cd56eebdd6cac01e45c6e3067
This commit is contained in:
Fimeg 2026-09-04 15:55:48 -04:00
commit 8f42fc953d
1476 changed files with 238455 additions and 0 deletions

View file

@ -0,0 +1,933 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.modules.common.functions
Singleton {
id: root
property string filePath: Directories.shellConfigPath
property alias options: configOptionsJsonAdapter
property bool ready: false
property int readWriteDelay: 50 // milliseconds
property bool blockWrites: false
// Tri-state option: "auto" (or anything unrecognized) defers to the
// caller's default, "on"/"off" force it. Booleans are honored so old
// configs written before the string form keep working.
function tristate(value, fallback) {
if (value === "on" || value === true) return true;
if (value === "off" || value === false) return false;
return fallback;
}
function setNestedValue(nestedKey, value) {
let keys = nestedKey.split(".");
let obj = root.options;
let parents = [obj];
// Traverse and collect parent objects
for (let i = 0; i < keys.length - 1; ++i) {
if (!obj[keys[i]] || typeof obj[keys[i]] !== "object") {
obj[keys[i]] = {};
}
obj = obj[keys[i]];
parents.push(obj);
}
// Convert value to correct type using JSON.parse when safe
let convertedValue = value;
if (typeof value === "string") {
let trimmed = value.trim();
if (trimmed === "true" || trimmed === "false" || !isNaN(Number(trimmed))) {
try {
convertedValue = JSON.parse(trimmed);
} catch (e) {
convertedValue = value;
}
}
}
obj[keys[keys.length - 1]] = convertedValue;
}
Timer {
id: fileReloadTimer
interval: root.readWriteDelay
repeat: false
onTriggered: {
configFileView.reload()
}
}
Timer {
id: fileWriteTimer
interval: root.readWriteDelay
repeat: false
onTriggered: {
configFileView.writeAdapter()
}
}
FileView {
id: configFileView
path: root.filePath
watchChanges: true
blockWrites: root.blockWrites
onFileChanged: fileReloadTimer.restart()
onAdapterUpdated: fileWriteTimer.restart()
onLoaded: root.ready = true
onLoadFailed: error => {
if (error == FileViewError.FileNotFound) {
writeAdapter();
}
}
JsonAdapter {
id: configOptionsJsonAdapter
property string panelFamily: "ii" // "ii", "waffle"
// Souveraine form-factor gate. Flip to true on a phone deploy:
// gates the Phone settings category and (later) the single-column
// layout. Laptop deploys stay false and see the stock page set.
property JsonObject souveraine: JsonObject {
property bool phone: false
}
property JsonObject policies: JsonObject {
property int ai: 1 // 0: No | 1: Yes | 2: Local
property int weeb: 1 // 0: No | 1: Open | 2: Closet
}
property JsonObject ai: JsonObject {
property string systemPrompt: "## Style\n- Use casual tone, don't be formal!\n- Always be brief and to the point, unless asked otherwise\n- Don't repeat the user's question\n- Be approachable: Avoid using overly complicated, domain-specific terms and provide analogies when asked to explain a concept\n\n## Context (ignore when irrelevant)\n- You are a helpful and inspiring sidebar assistant on a {DISTRO} Linux system\n- Desktop environment: {DE}\n- Current date & time: {DATETIME}\n- Focused app: {WINDOWCLASS}\n\n## Presentation\n- Use Markdown features in your response: \n - **Bold** text to **highlight keywords** in your response\n - **Split long information into small sections** with h2 headers and a relevant emoji at the start of it (for example `## 🐧 Linux`). Bullet points are preferred over long paragraphs, unless you're offering writing support or instructed otherwise by the user.\n- Asked to compare different options? You should firstly use a table to compare the main aspects, then elaborate or include relevant comments from online forums *after* the table. Make sure to provide a final recommendation for the user's use case!\n- Use LaTeX formatting for mathematical and scientific notations whenever appropriate. Enclose all LaTeX '$$' delimiters. NEVER generate LaTeX code in a latex block unless the user explicitly asks for it. DO NOT use LaTeX for regular documents (resumes, letters, essays, CVs, etc.).\n\nThanks!\n"
property string tool: "functions" // search, functions, or none
property list<var> extraModels: [
{
"api_format": "openai", // Most of the time you want "openai". Use "gemini" for Google's models
"description": "This is a custom model. Edit the config to add more! | Anyway, this is DeepSeek R1 Distill LLaMA 70B",
"endpoint": "https://openrouter.ai/api/v1/chat/completions",
"homepage": "https://openrouter.ai/deepseek/deepseek-r1-distill-llama-70b:free", // Not mandatory
"icon": "spark-symbolic", // Not mandatory
"key_get_link": "https://openrouter.ai/settings/keys", // Not mandatory
"key_id": "openrouter",
"model": "deepseek/deepseek-r1-distill-llama-70b:free",
"name": "Custom: DS R1 Dstl. LLaMA 70B",
"requires_key": true
}
]
}
property JsonObject appearance: JsonObject {
property bool extraBackgroundTint: true
property int fakeScreenRounding: 2 // 0: None | 1: Always | 2: When not fullscreen
property JsonObject fonts: JsonObject {
property string main: "Google Sans Flex"
property string numbers: "Google Sans Flex"
property string title: "Google Sans Flex"
property string iconNerd: "JetBrains Mono NF"
property string monospace: "JetBrains Mono NF"
property string reading: "Readex Pro"
property string expressive: "Space Grotesk"
}
property JsonObject transparency: JsonObject {
property bool enable: false
property bool automatic: true
property real backgroundTransparency: 0.11
property real contentTransparency: 0.57
}
property JsonObject wallpaperTheming: JsonObject {
property bool enableAppsAndShell: true
property bool enableQtApps: true
property bool enableTerminal: true
property JsonObject terminalGenerationProps: JsonObject {
property real harmony: 0.6
property real harmonizeThreshold: 100
property real termFgBoost: 0.35
property bool forceDarkMode: false
}
}
property JsonObject palette: JsonObject {
property string type: "auto" // Allowed: auto, scheme-content, scheme-expressive, scheme-fidelity, scheme-fruit-salad, scheme-monochrome, scheme-neutral, scheme-rainbow, scheme-tonal-spot
property string accentColor: ""
}
}
property JsonObject audio: JsonObject {
// Values in %
property JsonObject protection: JsonObject {
// Prevent sudden bangs
property bool enable: false
property real maxAllowedIncrease: 10
property real maxAllowed: 99
}
}
property JsonObject apps: JsonObject {
property string bluetooth: "kcmshell6 kcm_bluetooth"
property string changePassword: "kitty -1 --hold=yes fish -i -c 'passwd'"
property string network: "kcmshell6 kcm_networkmanagement"
property string manageUser: "kcmshell6 kcm_users"
property string networkEthernet: "kcmshell6 kcm_networkmanagement"
property string taskManager: "plasma-systemmonitor --page-name Processes"
property string terminal: "kitty -1" // This is only for shell actions
property string update: "kitty -1 --hold=yes fish -i -c 'pkexec pacman -Syu'"
property string volumeMixer: `~/.config/hypr/hyprland/scripts/launch_first_available.sh "pavucontrol-qt" "pavucontrol"`
}
property JsonObject background: JsonObject {
property JsonObject widgets: JsonObject {
property JsonObject clock: JsonObject {
property bool enable: true
property bool showOnlyWhenLocked: false
property string placementStrategy: "leastBusy" // "free", "leastBusy", "mostBusy"
property real x: 100
property real y: 100
property string style: "cookie" // Options: "cookie", "digital"
property string styleLocked: "cookie" // Options: "cookie", "digital"
property JsonObject cookie: JsonObject {
// aiStyling gates the categorize step in
// switchwall.sh (it must be true for wallpapers to
// be AI-categorized on apply). aiPreset is the
// SEPARATE gate for CookieClock applying a
// category preset over the user's configured clock
// style — split 2026-08-05 after turning on
// aiStyling rewrote the phone's clock
// (applyStyle writes these keys). Default off: the
// clock keeps whatever the user set.
property bool aiStyling: false
property bool aiPreset: false
property int sides: 14
property string dialNumberStyle: "full" // Options: "dots" , "numbers", "full" , "none"
property string hourHandStyle: "fill" // Options: "classic", "fill", "hollow", "hide"
property string minuteHandStyle: "medium" // Options "classic", "thin", "medium", "bold", "hide"
property string secondHandStyle: "dot" // Options: "dot", "line", "classic", "hide"
property string dateStyle: "bubble" // Options: "border", "rect", "bubble" , "hide"
property bool timeIndicators: true
property bool hourMarks: false
property bool dateInClock: true
property bool constantlyRotate: false
property bool useSineCookie: false
}
property JsonObject digital: JsonObject {
property bool adaptiveAlignment: true
property bool showDate: true
property bool animateChange: true
property bool vertical: false
property JsonObject font: JsonObject {
property string family: "Google Sans Flex"
property real weight: 350
property real width: 100
property real size: 90
property real roundness: 0
}
}
property JsonObject quote: JsonObject {
property bool enable: false
property string text: ""
}
}
property JsonObject weather: JsonObject {
property bool enable: false
property string placementStrategy: "free" // "free", "leastBusy", "mostBusy"
property real x: 400
property real y: 100
}
// Backported 2026-07-13 from laptop-upstream Config: the
// borrowed Background.qml reads these unconditionally, and
// their absence threw TypeErrors on the laptop. Keys only —
// we are NOT pulling the desktop-widget rendering machinery
// into this phone-first shell; enable defaults to false.
property JsonObject visualizer: JsonObject {
property bool enable: false
property string placementStrategy: "free"
property real x: 500
property real y: 500
property int bars: 30
property string style: "bars"
property bool vertical: false
}
property JsonObject stats: JsonObject {
property bool enable: false
property string placementStrategy: "free"
property real x: 150
property real y: 300
property string githubUsername: ""
property string codeforcesUsername: ""
property bool showGraphs: false
}
property JsonObject systemResources: JsonObject {
property bool enable: false
property string placementStrategy: "free"
property real x: 150
property real y: 600
property bool showGraphs: false
}
}
property string wallpaperPath: ""
property string thumbnailPath: ""
// Optional local derivatives for unlike display shapes. The
// original wallpaper remains canonical; variants are never a
// destructive replacement for it.
property string portraitVariantPath: ""
property string landscapeVariantPath: ""
property real wallpaperFocalX: 0.5
property real wallpaperFocalY: 0.5
property bool hideWhenFullscreen: true
property JsonObject parallax: JsonObject {
property bool vertical: false
property bool autoVertical: false
property bool enableWorkspace: false
property real workspaceZoom: 1.07 // Relative to wallpaper size
property bool enableSidebar: false
property real widgetsFactor: 1.2
}
}
property JsonObject bar: JsonObject {
// TASK-69/70: one collector for every agent session
// (Souveraine, Claude Code, Codex). Replaces per-provider
// fetches; claudeUsage above stays for the subscription
// gauge until stage 3 folds its OAuth poll in here.
property JsonObject agentSessions: JsonObject {
property bool enable: true // Show the agent island in the bar
property int refreshInterval: 60 // seconds
property int windowMinutes: 1440 // How far back counts as "a session"
}
// Bar layout. One BarContent serves every device; which
// widgets appear and in what order is data, not a fork.
// Known names: activeWindow, cellular, resources, media,
// claudeUsage, workspaces, clock, utilButtons, island,
// battery, pomodoro, systray. An unknown name renders
// nothing, so ["none"] deliberately empties a slot; an
// empty list means "unset" and falls back to the profile.
property JsonObject layout: JsonObject {
// "auto" derives from the bar's own cramped-ness test
// (the same one behind useShortenedForm), so a phone
// needs no config. "desktop" | "compact" pin it.
property string profile: "auto"
property list<string> left: []
property list<string> centerLeft: []
property list<string> centerMiddle: []
property list<string> centerRight: []
property list<string> right: []
}
property JsonObject clock: JsonObject {
property string format: "" // Empty: pick by profile
}
property JsonObject autoHide: JsonObject {
property bool enable: false
property int hoverRegionWidth: 2
property bool pushWindows: false
property JsonObject showWhenPressingSuper: JsonObject {
property bool enable: true
property int delay: 140
}
}
property bool bottom: false // Instead of top
property int cornerStyle: 0 // 0: Hug | 1: Float | 2: Plain rectangle
property bool floatStyleShadow: true // Show shadow behind bar when cornerStyle == 1 (Float)
property bool borderless: false // true for no grouping of items
property string topLeftIcon: "spark" // Options: "distro" or any icon name in ~/.config/quickshell/ii/assets/icons
property bool showBackground: true
property bool verbose: true
property bool vertical: false
property JsonObject resources: JsonObject {
// "auto" follows the active bar profile: compact bars
// rotate one stat; desktop bars render the full group.
// "on"/"off" override it. Never null — a null var is
// serialized into config.json and crashes JsonAdapter on
// the next load.
property string rotate: "auto"
property int rotateInterval: 4
property bool alwaysShowSwap: true
property bool alwaysShowCpu: true
property int memoryWarningThreshold: 95
property int swapWarningThreshold: 85
property int cpuWarningThreshold: 90
}
property list<string> screenList: [] // List of names, like "eDP-1", find out with 'hyprctl monitors' command
property JsonObject utilButtons: JsonObject {
property bool showScreenSnip: true
property bool showColorPicker: false
property bool showMicToggle: false
property bool showKeyboardToggle: true
property bool showDarkModeToggle: true
property bool showPerformanceProfileToggle: false
property bool showScreenRecord: false
}
property JsonObject workspaces: JsonObject {
property bool monochromeIcons: true
property int shown: 10
property bool showAppIcons: true
property bool alwaysShowNumbers: false
property int showNumberDelay: 300 // milliseconds
property list<string> numberMap: ["1", "2"] // Characters to show instead of numbers on workspace indicator
property bool useNerdFont: false
}
// Backported 2026-07-13: borrowed ClaudeUsage service + bar
// widget read these unconditionally. Keys only, enable=false.
property JsonObject claudeUsage: JsonObject {
property bool enable: false // Show a Claude (Pro/Max) subscription usage gauge in the bar
property bool defaultWeekly: false // Start on the 7-day window; click to switch session <-> week
property int warningThreshold: 90 // Turn red at/above this utilization (%)
property int fetchInterval: 5 // minutes
}
property JsonObject weather: JsonObject {
property bool enable: false
property bool enableGPS: true // gps based location
property string city: "" // When 'enableGPS' is false
property bool useUSCS: false // Instead of metric (SI) units
property int fetchInterval: 10 // minutes
}
property JsonObject indicators: JsonObject {
property JsonObject notifications: JsonObject {
property bool showUnreadCount: false
}
// Status icons inside the right-hand pill. "auto" follows
// the profile: a compact bar drops xkb and bluetooth.
// "on"/"off" override it. Never null — see bar.resources.
property string showXkb: "auto"
property string showBluetooth: "auto"
}
property JsonObject tooltips: JsonObject {
property bool clickToShow: false
}
}
property JsonObject battery: JsonObject {
property int low: 20
property int critical: 5
property int full: 101
property bool automaticSuspend: true
property int suspend: 3
}
property JsonObject calendar: JsonObject {
property string locale: "en-GB"
}
property JsonObject cheatsheet: JsonObject {
// Use a nerdfont to see the icons
// 0: 󰖳 | 1: 󰌽 | 2: 󰘳 | 3: | 4: 󰨡
// 5: | 6: | 7: 󰣇 | 8: | 9:
// 10: | 11: | 12: | 13: | 14: 󱄛
property string superKey: ""
property bool useMacSymbol: false
property bool splitButtons: false
property bool useMouseSymbol: false
property bool useFnSymbol: false
property JsonObject fontSize: JsonObject {
property int key: Appearance.font.pixelSize.smaller
property int comment: Appearance.font.pixelSize.smaller
}
}
property JsonObject conflictKiller: JsonObject {
property bool autoKillNotificationDaemons: false
// DO NOT auto-kill trays here. On this phone kded6 is not a
// competing Plasma tray — it is the StatusNotifierWatcher that
// ii's OWN tray (Quickshell.Services.SystemTray) registers as a
// host with (IsStatusNotifierHostRegistered=true). Killing it
// just makes D-Bus re-activate it on ii's next tray call — an
// infinite loop. Our services/ConflictKiller.qml override drops
// the kded6 check entirely. Leave this false.
property bool autoKillTrays: false
}
property JsonObject crosshair: JsonObject {
// Valorant crosshair format. Use https://www.vcrdb.net/builder
property string code: "0;P;d;1;0l;10;0o;2;1b;0"
}
property JsonObject dock: JsonObject {
property bool enable: false
property bool monochromeIcons: true
property real height: 60
property real hoverRegionHeight: 2
// Home normally keeps its dock visible. Auto-hide leaves only
// the edge reveal strip mapped, then raises the same dock when
// the pointer dwells there. These delays belong to the dock's
// one visibility state machine; Settings only writes them.
property bool autoHide: false
property int revealDelayMs: 120
property int hideDelayMs: 350
property bool pinnedOnStartup: false
property bool hoverToReveal: true // When false, only reveals on empty workspace
property list<string> pinnedApps: [ // IDs of pinned entries
"org.kde.dolphin", "kitty",]
property list<string> ignoredAppRegexes: []
// Fan-out stacks (macOS "Fan" mode). Each entry is
// "stackId|appId,appId,appId" — phone-local, no Phosh sync.
// stackId is a display label; members render as an arc.
property list<string> stacks: []
// Feel knobs (single source — components read these, never
// hardcode): dragDwellMs = how long a drag hovers an icon
// or stack before it reads as combine-intent (GNOME's
// rule); gestureRailHeight = bottom strip the dock reserves
// for Souveraine's integrated navigation rail, visually and
// in its input mask.
property int dragDwellMs: 500
property real gestureRailHeight: 32
// How big the dock draws. `buttonSize` is the one number the
// row's height follows and every icon is derived from, so the
// dock scales as a piece rather than each part being tuned
// apart. 56/44 replaced a hardcoded 44/35 that read small on a
// 540px panel; it lives here so it is a setting rather than a
// rebuild.
property real buttonSize: 56
property real iconSize: 44
}
property JsonObject interactions: JsonObject {
property JsonObject scrolling: JsonObject {
property bool fasterTouchpadScroll: false // Enable faster scrolling with touchpad
property int mouseScrollDeltaThreshold: 120 // delta >= this then it gets detected as mouse scroll rather than touchpad
property int mouseScrollFactor: 120
property int touchpadScrollFactor: 450
}
property JsonObject deadPixelWorkaround: JsonObject { // Hyprland leaves out 1 pixel on the right for interactions
property bool enable: false
}
}
property JsonObject language: JsonObject {
property string ui: "auto" // UI language. "auto" for system locale, or specific language code like "zh_CN", "en_US"
property JsonObject translator: JsonObject {
property string engine: "auto" // Run `trans -list-engines` for available engines. auto should use google
property string targetLanguage: "auto" // Run `trans -list-all` for available languages
property string sourceLanguage: "auto"
}
}
property JsonObject launcher: JsonObject {
property list<string> pinnedApps: [ "org.kde.dolphin", "kitty", "cmake-gui"]
}
property JsonObject light: JsonObject {
property JsonObject night: JsonObject {
property bool automatic: true
property string from: "19:00" // Format: "HH:mm", 24-hour time
property string to: "06:30" // Format: "HH:mm", 24-hour time
property int colorTemperature: 5000
}
property JsonObject antiFlashbang: JsonObject {
property bool enable: false
}
}
property JsonObject lock: JsonObject {
property bool useHyprlock: false
property bool launchOnStartup: false
property int dpmsTimeout: 3
property string unlockHook: ""
// Souveraine: PIN keypad surface for touch panels (phone).
// The OSK can't rise above a session lock, so the lock
// surface must carry its own input.
property bool touchKeypad: false
property JsonObject blur: JsonObject {
property bool enable: true
property real radius: 100
property real extraZoom: 1.1
}
property bool centerClock: true
// 12-hour glance clock (am/pm). Off = 24-hour.
property bool twelveHourClock: true
// Lock-screen wallpaper. Empty = follow the system wallpaper
// (background.wallpaperPath); a path pins the lock's own.
property string wallpaperPath: ""
property bool showLockedText: true
// A visible wiring exercise for the FPC1020 path. It never
// unlocks the session; the separate Polkit setting below is
// the temporary, post-login-only factor.
property JsonObject fingerprintPreview: JsonObject {
property bool enabled: false
property int holdMs: 3000
}
// The temporary FPC factor belongs only to polkit-1. It cannot
// satisfy first login, disk unlock, sudo, SSH, or lock-screen
// PAM. The real souveraine-fpd match result will replace this
// raw-reader bridge without changing the user-facing surface.
property JsonObject fingerprintPolkit: JsonObject {
property bool enabled: true
}
// Souveraine-owned lock cards. Transport controls are
// ambient; media metadata remains personal by default.
property JsonObject content: JsonObject {
property bool showMediaControls: true
property bool mediaMetadataAmbient: false
property bool showBattery: true
// App identity + count are ambient; the summary line is
// personal until promoted. Bodies never render on lock.
property bool showNotifications: true
property bool notificationContentAmbient: false
}
property JsonObject idle: JsonObject {
// Native idle-notify remains opt-in until verified on the
// Pixel compositor; hypridle is the current adapter.
property bool nativeCoordinatorEnabled: false
// The lock timer is the one the user sets. The dim is
// expressed RELATIVE to it — how long before the lock the
// screen starts fading — so the two can never cross and
// moving one moves the other. They were independent
// absolutes (120 and 300), which meant picking a dim
// longer than the lock silently produced a dim that never
// fired. sessiond's lock-screen dim has always been
// relative (`dim_at = budget - dim_grace`); this is the
// shell saying the same thing the same way.
//
// Default 180 reproduces the old pair exactly: 300 - 180 =
// 120. An existing config.json keeps its lockAfterSeconds
// and picks this default up, so nobody's timing changes.
property int lockAfterSeconds: 300
property int dimBeforeLockSeconds: 180
// Backlight level while Dimmed; Active restores the
// brightnessctl-saved value.
property string dimBrightness: "30%"
}
property JsonObject security: JsonObject {
property bool unlockKeyring: true
property bool requirePasswordToPower: false
// Whether the lock surface may trigger power-off / reboot
// at all. Off by default (fail-closed): a destructive
// action from the locked screen is opt-in. The system
// polkit rule (souveraine-login1.rules) lets the active
// session power off without challenge once unlocked; this
// gate keeps the *locked* surface from reaching it until
// the user enables it. requirePasswordToPower then arms
// the action to be confirmed by the PIN unlock.
property bool allowPowerFromLock: false
}
// Step-up authentication grants for sensitive operations.
// The PAM service (souveraine-stepup) is root-owned system
// config and must be installed separately before enabling.
property JsonObject stepUp: JsonObject {
property int grantTtlMs: 300000 // 5 minutes
property bool enabled: false // requires souveraine-stepup PAM service
}
property bool materialShapeChars: true
}
property JsonObject media: JsonObject {
// Attempt to remove dupes (the aggregator playerctl one and browsers' native ones when there's plasma browser integration)
property bool filterDuplicatePlayers: true
}
property JsonObject networking: JsonObject {
property string userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"
}
property JsonObject notifications: JsonObject {
property int timeout: 7000
// Upstream modules (NotificationPopup, settings InterfaceConfig)
// read forceMonitor; the old "monitor" name left that undefined
// and threw a TypeError on every popup evaluation.
property JsonObject forceMonitor: JsonObject {
property bool enable: false
property string name: "" // Name of the monitor to show notifications on, like "eDP-1". Find out with 'hyprctl monitors' command
}
}
property JsonObject osd: JsonObject {
property int timeout: 1000
}
property JsonObject osk: JsonObject {
property string layout: "qwerty_full"
property bool pinnedOnStartup: false
}
// Speech services — network STT/TTS on Casey's VPN. The shell
// and the souveraine-stt/tts CLIs read the same config file, so
// the settings page here is the single source of truth for both.
property JsonObject speech: JsonObject {
property JsonObject stt: JsonObject {
property bool enable: true
// faster-whisper REST bridge: POST multipart 'audio'
// (WAV) -> {"text": ...}. /health for the status probe.
property string endpoint: ""
}
property JsonObject tts: JsonObject {
property bool enable: false
// Speech-synthesis endpoint: POST {"text": ...} -> audio
// stream. Empty until a TTS server exists on the VPN.
property string endpoint: ""
}
}
// Text-selection action menu (TASK-18).
//
// OPT-IN ON PURPOSE, and do not flip this default casually. When
// enabled, the shell watches the compositor's primary selection,
// which means it observes EVERY piece of text the user highlights
// anywhere on the device — including a password highlighted inside
// a password manager. No tier in SESSION-AUTHORITY-DOCTRINE §2
// covers an ambient capability of that reach, so the user turns it
// on knowingly or it does not run. Selection.qml additionally kills
// its watcher whenever the session is locked and never persists or
// logs selection content.
property JsonObject selection: JsonObject {
property bool enable: false
// Read Aloud is the one action with a live backend today; it
// routes through Speech.speak() and so also obeys
// speech.tts.enable. The rest are stubs until they earn a
// backend (TASK-18: a stubbed action is fine, a janky menu is not).
property bool readAloud: true
// Agent actions ("Talk about this" / "Add to our conversation")
// reach conversation history, which is `personal` tier — they
// stay dark until the session is genuinely unlocked.
property bool agentActions: true
// Reference lookups stay strictly local/offline by default,
// per TASK-18's stated default stance: never touch the agent.
property bool referenceActions: true
}
property JsonObject overlay: JsonObject {
property bool openingZoomAnimation: true
property bool darkenScreen: true
property real clickthroughOpacity: 0.8
property JsonObject floatingImage: JsonObject {
property string imageSource: "https://media.tenor.com/H5U5bJzj3oAAAAAi/kukuru.gif"
property real scale: 0.5
}
}
property JsonObject overview: JsonObject {
property bool enable: true
property real scale: 0.18 // Relative to screen size
property real rows: 2
property real columns: 5
property bool orderRightLeft: false
property bool orderBottomUp: false
property bool centerIcons: true
// The app drawer's own grid (TASK-14). Kept nested instead of
// overloading rows/columns above — those are the desktop
// overview's workspace-grid knobs, and the drawer is a
// different density. The settings app writes these.
property JsonObject appGrid: JsonObject {
property int columns: 4
property int rows: 5
property int iconSize: 44
}
}
property JsonObject regionSelector: JsonObject {
property JsonObject targetRegions: JsonObject {
property bool windows: true
property bool layers: false
property bool content: true
property bool showLabel: false
property real opacity: 0.3
property real contentRegionOpacity: 0.8
property int selectionPadding: 5
}
property JsonObject rect: JsonObject {
property bool showAimLines: true
}
property JsonObject circle: JsonObject {
property int strokeWidth: 6
property int padding: 10
}
property JsonObject annotation: JsonObject {
property bool useSatty: false
}
}
property JsonObject resources: JsonObject {
property int updateInterval: 3000
property int historyLength: 60
}
property JsonObject tray: JsonObject {
property bool monochromeIcons: true
property bool showItemId: false
property bool invertPinnedItems: true // Makes the below a whitelist for the tray and blacklist for the pinned area
property list<var> pinnedItems: [ "Fcitx" ]
property bool filterPassive: true
}
property JsonObject musicRecognition: JsonObject {
property int timeout: 16
property int interval: 4
}
property bool autoIdleInhibit: false
property JsonObject search: JsonObject {
property int nonAppResultDelay: 30 // This prevents lagging when typing
property string engineBaseUrl: "https://www.google.com/search?q="
property list<string> excludedSites: ["quora.com", "facebook.com"]
property bool sloppy: false // Uses levenshtein distance based scoring instead of fuzzy sort. Very weird.
property JsonObject prefix: JsonObject {
property bool showDefaultActionsWithoutPrefix: true
property string action: "/"
property string app: ">"
property string clipboard: ";"
property string emojis: ":"
property string math: "="
property string shellCommand: "$"
property string webSearch: "?"
}
property JsonObject imageSearch: JsonObject {
property string imageSearchEngineBaseUrl: "https://lens.google.com/uploadbyurl?url="
property bool useCircleSelection: false
}
// Backported 2026-07-13: borrowed LauncherSearch reads this
// unconditionally. Key only, enable=false.
property JsonObject fileSearch: JsonObject {
property bool enable: false
property list<string> paths: [Directories.home]
property list<string> exclude: [".git", "node_modules", "target", "build"]
property bool excludeHiddenDirs: true
property int maxResults: 30
}
}
property JsonObject sidebar: JsonObject {
property bool keepRightSidebarLoaded: true
property int width: 460 // Collapsed sidebar width in px
property int widthExtended: 750 // Extended (Ctrl+O) sidebar width in px
property JsonObject translator: JsonObject {
property bool enable: false
property int delay: 300 // Delay before sending request. Reduces (potential) rate limits and lag.
}
property JsonObject ai: JsonObject {
property bool textFadeIn: false
// Chat message text size. Live binding to the theme value
// (never a literal, and never null — see
// null-in-a-serialized-config, 2026-08-11). ii-base added
// this key after our override was forked, and because this
// file SHADOWS ii-base's Config wholesale, the key simply
// vanished: MessageTextBlock read it as undefined and
// assigned undefined to font.pixelSize on every render.
property int fontSize: Appearance.font.pixelSize.small
}
property JsonObject booru: JsonObject {
property bool allowNsfw: false
property string defaultProvider: "yandere"
property int limit: 20
property JsonObject zerochan: JsonObject {
property string username: "[unset]"
}
}
property JsonObject cornerOpen: JsonObject {
property bool enable: true
property bool bottom: false
property bool valueScroll: true
property bool clickless: false
property int cornerRegionWidth: 250
property int cornerRegionHeight: 5
property bool visualize: false
property bool clicklessCornerEnd: true
property int clicklessCornerVerticalOffset: 1
}
property JsonObject quickToggles: JsonObject {
property string style: "android" // Options: classic, android
property JsonObject android: JsonObject {
property int columns: 5
property list<var> toggles: [
{ "size": 2, "type": "network" },
{ "size": 2, "type": "bluetooth" },
{ "size": 1, "type": "idleInhibitor" },
{ "size": 1, "type": "mic" },
{ "size": 2, "type": "audio" },
{ "size": 2, "type": "nightLight" }
]
}
}
property JsonObject quickSliders: JsonObject {
property bool enable: false
property bool showMic: false
property bool showVolume: true
property bool showBrightness: true
}
}
property JsonObject screenRecord: JsonObject {
property string savePath: Directories.videos.replace("file://","") // strip "file://"
}
property JsonObject screenSnip: JsonObject {
property string savePath: "" // only copy to clipboard when empty
}
property JsonObject sounds: JsonObject {
property bool battery: false
property bool pomodoro: false
property string theme: "freedesktop"
}
property JsonObject time: JsonObject {
// https://doc.qt.io/qt-6/qtime.html#toString
property string format: "hh:mm"
property string shortDateFormat: "dd/MM"
property string dateWithYearFormat: "dd/MM/yyyy"
property string dateFormat: "ddd, dd/MM"
property JsonObject pomodoro: JsonObject {
property int breakTime: 300
property int cyclesBeforeLongBreak: 4
property int focus: 1500
property int longBreak: 900
}
property bool secondPrecision: false
}
property JsonObject updates: JsonObject {
property bool enableCheck: true
property int checkInterval: 120 // minutes
property int adviseUpdateThreshold: 75 // packages
property int stronglyAdviseUpdateThreshold: 200 // packages
}
property JsonObject wallpaperSelector: JsonObject {
property bool useSystemFileDialog: false
}
property JsonObject windows: JsonObject {
property bool showTitlebar: true // Client-side decoration for shell apps
property bool centerTitle: true
}
property JsonObject hacks: JsonObject {
property int arbitraryRaceConditionDelay: 20 // milliseconds
}
property JsonObject workSafety: JsonObject {
property JsonObject enable: JsonObject {
property bool wallpaper: false
property bool clipboard: false
}
property JsonObject triggerCondition: JsonObject {
property list<string> networkNameKeywords: ["airport", "cafe", "college", "company", "eduroam", "free", "guest", "public", "school", "university"]
property list<string> fileKeywords: ["anime", "booru", "ecchi", "hentai", "yande.re", "konachan", "breast", "nipples", "pussy", "nsfw", "spoiler", "girl"]
property list<string> linkKeywords: ["hentai", "porn", "sukebei", "hitomi.la", "rule34", "gelbooru", "fanbox", "dlsite"]
}
}
property JsonObject waffles: JsonObject {
// Some spots are kinda janky/awkward. Setting the following to
// false will make (some) stuff also be like that for accuracy.
// Example: the right-click menu of the Start button
property JsonObject tweaks: JsonObject {
property bool switchHandlePositionFix: true
property bool smootherMenuAnimations: true
property bool smootherSearchBar: true
}
property JsonObject bar: JsonObject {
property bool bottom: true
property bool leftAlignApps: false
}
property JsonObject actionCenter: JsonObject {
property list<string> toggles: [ "network", "vpn", "bluetooth", "easyEffects", "powerProfile", "idleInhibitor", "nightLight", "darkMode", "antiFlashbang", "cloudflareWarp", "mic", "musicRecognition", "notifications", "onScreenKeyboard", "gameMode", "screenSnip", "colorPicker" ]
}
property JsonObject calendar: JsonObject {
property bool force2CharDayOfWeek: true
}
}
}
}
}

View file

@ -0,0 +1,216 @@
// Souveraine patch to ii's stock Persistent.qml.
//
// Adds states.lock.locked: written by LockScreen.qml whenever the session
// lock engages/releases, so a quickshell crash or restart while locked
// comes back locked instead of silently dropping the lock. Everything else
// is unchanged stock ii — diff against upstream before re-applying if ii
// updates.
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
property alias states: persistentStatesJsonAdapter
property string fileDir: Directories.state
property string fileName: "states.json"
property string filePath: `${root.fileDir}/${root.fileName}`
property bool ready: false
property string previousHyprlandInstanceSignature: ""
// What identifies THIS compositor run.
//
// `HYPRLAND_INSTANCE_SIGNATURE` is unset under viewtop, so this compared
// "" to "" and `isNewHyprlandInstance` was false every single time. It
// gates `lock.launchOnStartup` (LockScreen.qml), which therefore never
// fired once, and both Idle.qml copies read it too. viewtop publishes
// `viewtop.instance` beside its control socket for exactly this: pid plus
// startup nanos, different on every start.
//
// The env var stays as the fallback so this file still behaves on a
// Hyprland session — the laptop is still one — and an empty answer from
// both means "assume the session continues", which re-locks rather than
// assuming a fresh boot.
readonly property string instanceSignature: viewtopInstance.text().trim()
|| Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE")
|| ""
property bool isNewHyprlandInstance: previousHyprlandInstanceSignature !== states.hyprlandInstanceSignature
FileView {
id: viewtopInstance
path: `${Quickshell.env("XDG_RUNTIME_DIR") || "/run/user/1000"}/souveraine/viewtop.instance`
// Absence is normal, not an error: a Hyprland session has no such file
// and falls through to the env var below.
printErrors: false
// Synchronously, because `onReadyChanged` reads `text()` exactly once
// to decide whether this is a new session. Loaded async it was still
// empty at that moment, fell through to the unset env var, and the
// signature persisted as "" — the same bug this replaced, arrived at
// by a different route.
blockLoading: true
}
onReadyChanged: {
root.previousHyprlandInstanceSignature = root.states.hyprlandInstanceSignature
root.states.hyprlandInstanceSignature = root.instanceSignature
}
Timer {
id: fileReloadTimer
interval: 100
repeat: false
onTriggered: {
persistentStatesFileView.reload()
}
}
Timer {
id: fileWriteTimer
interval: 100
repeat: false
onTriggered: {
persistentStatesFileView.writeAdapter()
}
}
FileView {
id: persistentStatesFileView
path: root.filePath
watchChanges: true
onFileChanged: fileReloadTimer.restart()
onAdapterUpdated: fileWriteTimer.restart()
onLoaded: root.ready = true
onLoadFailed: error => {
console.log("Failed to load persistent states file:", error);
if (error == FileViewError.FileNotFound) {
fileWriteTimer.restart();
}
}
adapter: JsonAdapter {
id: persistentStatesJsonAdapter
property string hyprlandInstanceSignature: ""
property JsonObject ai: JsonObject {
property string model: "gemini-2.5-flash"
property real temperature: 0.5
}
property JsonObject cheatsheet: JsonObject {
property int tabIndex: 0
}
property JsonObject sidebar: JsonObject {
property JsonObject bottomGroup: JsonObject {
property bool collapsed: false
property int tab: 0
}
}
property JsonObject booru: JsonObject {
property bool allowNsfw: false
property string provider: "yandere"
}
property JsonObject idle: JsonObject {
property bool inhibit: false
}
property JsonObject lock: JsonObject {
property bool locked: false
}
// Navigation-rail onboarding. `missionControlDiscovered` flips
// true the first time the triple-swipe raises Mission Control;
// until then the rail may nudge the gesture after repeated
// incomplete swipes (the Souveraine analogue of Launcher3's
// AllAppsEduView, driven from SystemGestureRail.qml). Persisted so
// the nudge doesn't return after the user has found the gesture.
property JsonObject navigation: JsonObject {
property bool missionControlDiscovered: false
}
property JsonObject overlay: JsonObject {
property list<string> open: ["crosshair", "recorder", "volumeMixer", "resources"]
property JsonObject crosshair: JsonObject {
property bool pinned: false
property bool clickthrough: true
property real x: 827
property real y: 441
property real width: 250
property real height: 100
}
property JsonObject floatingImage: JsonObject {
property bool pinned: false
property bool clickthrough: false
property real x: 1650
property real y: 390
property real width: 0
property real height: 0
}
property JsonObject fpsLimiter: JsonObject {
property bool pinned: false
property bool clickthrough: false
property real x: 1570
property real y: 615
property real width: 280
property real height: 80
}
property JsonObject recorder: JsonObject {
property bool pinned: false
property bool clickthrough: false
property real x: 80
property real y: 80
property real width: 350
property real height: 130
}
property JsonObject resources: JsonObject {
property bool pinned: false
property bool clickthrough: true
property real x: 1500
property real y: 770
property real width: 350
property real height: 200
property int tabIndex: 0
}
property JsonObject volumeMixer: JsonObject {
property bool pinned: false
property bool clickthrough: false
property real x: 80
property real y: 280
property real width: 350
property real height: 600
property int tabIndex: 0
}
property JsonObject notes: JsonObject {
property bool pinned: false
property bool clickthrough: true
property real x: 1400
property real y: 42
property real width: 460
property real height: 330
}
}
property JsonObject timer: JsonObject {
property JsonObject pomodoro: JsonObject {
property bool running: false
property int start: 0
property bool isBreak: false
property int cycle: 0
}
property JsonObject stopwatch: JsonObject {
property bool running: false
property int start: 0
property list<var> laps: []
}
}
}
}
}

View file

@ -0,0 +1,215 @@
// Shell layer/state registry — the declarative surface model.
//
// One place that says, for each meaningful surface: which layer-shell layer
// it lives on, which GlobalStates bit(s) gate its visibility, and whether it
// is active right now (derived from the live state). This is the registry
// half of the layer-registry + ShellState work
// (docs/tasks/souveraine-shell-ecosystem.md, section 1).
//
// This is a QtObject instantiated inside Dock.qml's Scope (as
// ShellModelLocal.ShellModel), NOT a qs.services singleton — same reason as
// DockManifest: GlobalStates.qml imports qs.services, so a qs.services
// singleton that imports qs for GlobalStates forms a circular import QML
// cannot resolve. As a local type under modules/common it sidesteps that
// cycle. It carries its own imports (qs, qs.services, qs.modules.common,
// QtQuick) because a local type does NOT inherit the importing file's
// imports.
//
// IMPORTANT — what this is NOT:
// - It does NOT change how panels render or claim their layer. Panels keep
// their ad-hoc WlrLayershell.layer / exclusiveZone bindings. Making
// panels READ from this registry is a later, riskier step.
// - It is a read-only projection that mirrors the truth, so the agent and
// external callers can ask "what surfaces exist, what layer, what state
// gates each, what is active" without parsing QML.
//
// Layer vocabulary mirrors the wlr-layer-shell protocol quickshell exposes
// (Quickshell.Wayland.WlrLayer): Background < Bottom < Top < Overlay. The
// lock surface is separate — it uses ext-session-lock-v1
// (WlSessionLockSurface), not the layer shell — recorded here as layer
// "session-lock" so consumers can tell the mechanisms apart.
//
// Quickshell's PanelWindow default layer is Top when WlrLayershell.layer is
// unset, which is how several surfaces (dock, bar, sidebarLeft, sidebarRight)
// end up on Top implicitly. Those defaults are reflected here as "top" so the
// registry matches what actually renders, not just what is declared.
import qs
import qs.services
import qs.modules.common
import QtQuick
QtObject {
id: root
// --- The declarative registry --------------------------------------
//
// Each entry is a plain object the projection returns verbatim. Fields:
// name stable id, matches the family/panel name
// layer "background" | "bottom" | "top" | "overlay" | "session-lock"
// namespace the wlr layer namespace the panel claims, when known
// layerRule "declared" (set explicitly in QML) | "default-top"
// (PanelWindow default — no explicit WlrLayershell.layer)
// | "session-lock" (ext-session-lock-v1, not layer shell)
// gateStates array of GlobalStates property names whose truth gates
// visibility (the state that drives the surface being
// shown). Empty for always-resident structural surfaces.
// gateConfig Config.options.* path that enables the surface at all
// (the family extraCondition), "" when none.
// activeExpr short human-readable description of when active.
readonly property var _surfaceDefs: [
{
name: "background",
layer: "background",
namespace: "quickshell:background",
layerRule: "declared",
gateStates: [],
gateConfig: "",
activeExpr: "always (resident); raises to overlay when screenLocked"
},
{
name: "bar",
layer: "top",
namespace: "quickshell:bar",
layerRule: "default-top",
gateStates: ["barOpen"],
gateConfig: "options.bar.vertical (excludes verticalBar)",
activeExpr: "barOpen && !options.bar.vertical"
},
{
name: "verticalBar",
layer: "top",
namespace: "quickshell:verticalBar",
layerRule: "default-top",
gateStates: ["barOpen"],
gateConfig: "options.bar.vertical",
activeExpr: "barOpen && options.bar.vertical"
},
{
name: "dock",
layer: "top",
namespace: "quickshell:dock",
layerRule: "default-top",
gateStates: ["dockRevealed"],
gateConfig: "options.dock.enable",
activeExpr: "options.dock.enable && Dock.computeDockState() !== Hidden \u2014 pinned on home, hidden on every other zone; suppressed by oskOpen + screenLocked"
},
{
name: "sidebarLeft",
layer: "top",
namespace: "quickshell:sidebarLeft",
layerRule: "default-top",
gateStates: ["sidebarLeftOpen"],
gateConfig: "",
activeExpr: "sidebarLeftOpen"
},
{
name: "sidebarRight",
layer: "top",
namespace: "quickshell:sidebarRight",
layerRule: "default-top",
gateStates: ["sidebarRightOpen"],
gateConfig: "",
activeExpr: "sidebarRightOpen"
},
{
name: "overview",
layer: "top",
namespace: "quickshell:overview",
layerRule: "declared",
gateStates: ["overviewOpen"],
gateConfig: "",
activeExpr: "overviewOpen"
},
{
name: "onScreenKeyboard",
layer: "overlay",
namespace: "quickshell:onScreenKeyboard",
layerRule: "declared",
gateStates: ["oskOpen"],
gateConfig: "",
activeExpr: "oskOpen"
},
{
name: "lock",
layer: "session-lock",
namespace: "",
layerRule: "session-lock",
gateStates: ["screenLocked"],
gateConfig: "",
activeExpr: "screenLocked (ext-session-lock-v1, not layer shell)"
}
]
// --- Read projections ----------------------------------------------
// surfaces() — the registry list with a live `active` flag per entry.
// Shape rhymes with DockManifest.manifest(): a plain JS object array
// safe to serialize and hand to the agent / settings app.
function surfaces() {
return root._surfaceDefs.map(s => Object.assign({}, s, {
active: root._isActive(s.name)
}));
}
// state() — the current GlobalStates bits that matter for layer gating,
// plus the high-level shell mode. Read-only snapshot.
function state() {
return {
mode: Config.options?.souveraine?.phone ? "phone" : "desktop",
barOpen: GlobalStates.barOpen,
oskOpen: GlobalStates.oskOpen,
screenLocked: GlobalStates.screenLocked,
overviewOpen: GlobalStates.overviewOpen,
sidebarLeftOpen: GlobalStates.sidebarLeftOpen,
sidebarRightOpen: GlobalStates.sidebarRightOpen,
dockRevealed: GlobalStates.dockRevealed,
dockSuppressed: GlobalStates.dockSuppressed,
dockDragInProgress: GlobalStates.dockDragInProgress,
overlayOpen: GlobalStates.overlayOpen
};
}
// --- Internal: derive active from the live state -------------------
//
// The single source of truth for "is this surface showing right now" is
// the GlobalStates bit that gates it. The dock is special-cased because
// its visibility is computed (pinned / revealed / shown-on-empty-desktop)
// rather than a bare boolean; we approximate "active" as the dock's own
// manifest hidden flag so we never disagree with the dock about itself.
function _isActive(name) {
switch (name) {
case "background":
return true; // always resident
case "bar":
return GlobalStates.barOpen && !Config.options?.bar?.vertical;
case "verticalBar":
return GlobalStates.barOpen && !!Config.options?.bar?.vertical;
case "sidebarLeft":
return GlobalStates.sidebarLeftOpen;
case "sidebarRight":
return GlobalStates.sidebarRightOpen;
case "overview":
return GlobalStates.overviewOpen;
case "onScreenKeyboard":
return GlobalStates.oskOpen;
case "lock":
return GlobalStates.screenLocked;
case "dock":
return root._dockActive();
}
return false;
}
// Bound by Dock.qml, which instantiates this. The fallback ladder that
// stood here read GlobalStates only, so it could not see the home-zone
// rule and called the dock inactive on home.
property string dockVisibility: "hidden"
function _dockActive() {
if (GlobalStates.screenLocked) return false;
return root.dockVisibility !== "hidden";
}
}

View file

@ -0,0 +1,588 @@
// Souveraine fork of ii's stock Session.qml.
//
// Upstream ii's Session is a set of fire-and-forget verbs:
// Quickshell.execDetached(["bash", "-c", "systemctl poweroff || loginctl poweroff"])
// That is fine for a desktop where a failed poweroff is visible to the person
// sitting at the keyboard. It is not fine for the phone, where the shell is
// the only session manager and an agent can drive these verbs over IPC. A
// verb that silently does nothing is the worst outcome: the caller believes
// the machine is suspending and it is not.
//
// So this fork keeps every upstream verb (call sites in LockScreen.qml and
// the session menus are unchanged) and adds the parts a real session arbiter
// needs:
//
// 1. Capability detection. We query logind's Can* methods once at startup
// instead of treating a command being installed, or /sys/power/state
// advertising "disk", as proof that an action is usable.
// caps() reports what this machine can actually do, so a caller can ask
// before it acts and the session menu can grey out what is unavailable.
//
// 2. Honest failure. Upstream execDetached throws the exit code away. Every
// verb here runs through a Process with an onExited that logs
// [session] <verb> failed (exit N) and emits actionFailed(). A wedged
// logind is now a fact in the log, not silence.
//
// 3. Reason-tracked inhibits. Idle.qml's inhibit is a bare bool: something
// is holding the machine awake and nothing records what or why. inhibit()
// takes a reason, returns a cookie, and state() lists every holder. "Why
// is the phone not sleeping" becomes a question with an answer.
//
// 4. State that is re-derived, not cached. `secure` is WlSessionLock's
// compositor acknowledgement; it is distinct from `lockRequested`, the
// shell input that asks WlSessionLock to lock.
//
// The trust boundary here is deliberately trivial and stated so it stays that
// way: this surface is local, single-user, reachable only over quickshell's
// IPC socket by the user who owns the session. It has no remote caller and no
// second operator, so it has no grants, no signing, and no nonces. If it ever
// grows a network-reachable caller, that assumption is what breaks first.
pragma Singleton
import qs
import qs.services
import qs.modules.common
import Quickshell
import Quickshell.Io
import Quickshell.Services.Mpris
Singleton {
id: root
// --- Capabilities ------------------------------------------------------
// Probed once, at startup. Until the probe returns, every capability reads
// false: better to refuse a suspend we are unsure of than to fire a verb
// into a machine that cannot honor it.
property bool probed: false
property bool hasLoginctl: false
property bool hasSystemctl: false
property string suspendCapability: "unknown"
property string hibernateCapability: "unknown"
property string poweroffCapability: "unknown"
property string rebootCapability: "unknown"
// logind is the preferred backend when present: it is the thing that
// actually owns the session, and it works under elogind as well as
// systemd. systemctl is the fallback for the poweroff/reboot verbs.
// "challenge" means logind can do it after polkit authentication. It is
// available to a normal desktop session with a functioning polkit agent,
// but callers still learn that a prompt may be required through caps().
readonly property bool canSuspend: ["yes", "challenge"].includes(root.suspendCapability)
readonly property bool canHibernate: ["yes", "challenge"].includes(root.hibernateCapability)
readonly property bool canPoweroff: ["yes", "challenge"].includes(root.poweroffCapability)
readonly property bool canReboot: ["yes", "challenge"].includes(root.rebootCapability)
// Live compositor acknowledgement, mirrored from WlSessionLock.secure by
// LockScreen.qml. `screenLocked` remains the requested state that drives
// the lock surface; do not treat it as proof that the session is secure.
readonly property bool locked: GlobalStates.screenLockSecure
// Report the SECURE lock state to logind as the session's LockedHint —
// the freedesktop contract's state half (loginctl lock-session above is
// only the request half). This makes `LockedHint` truthful device-wide,
// so logind-aware apps (culver stops its Matrix sync, media players
// pause, recorders blank) can watch one standard property instead of
// each growing a bespoke shell IPC. Reporting the secure edge — not the
// requested edge — means the hint never claims locked before the
// compositor holds the lock. set-locked-hint is not a Lock signal, so
// the hypridle echo storm documented in lock() cannot recur through it.
onLockedChanged: {
if (root.hasLoginctl)
lockedHintProc.report(root.locked);
}
// Replay the hint once the capability probe lands.
//
// `hasLoginctl` starts false and only becomes true when `capabilityProbe`
// returns, which is a `Process` round trip. The lock goes secure long
// before that: measured 2026-08-02, `secure=true` at 15:21:07 and
// `loginctl=true` at 15:21:37 — thirty seconds later. The one edge that
// mattered was therefore dropped by the guard above and never retried,
// because the shell locks once at boot and `locked` never changes again.
//
// The cost was the whole lock-before-blank invariant. `LockedHint` stayed
// `no`, so sessiond's `locked` (which comes from logind, doctrine §4) was
// permanently false, `request_blank()` timed out its `LOCK_ACK_BUDGET`
// every time, and the panel blanked on a session nobody could confirm was
// locked — `blank-without-lock` on every single blank.
//
// This is the fourth edge-vs-level bug in this system after `locked_ack`,
// `ChargeRate` and `bootBloomActive`. A guard that drops a report must
// replay it when the guard opens, or the report is only ever delivered by
// luck of ordering.
onHasLoginctlChanged: {
if (root.hasLoginctl)
lockedHintProc.report(root.locked);
}
// The seat's session object path, resolved once.
//
// `report()` used to resolve it on every call, which meant `sh` plus two
// busctl round trips before the hint could move. Measured 2026-08-02: the
// hint landed *after* sessiond's 2 s `LOCK_ACK_BUDGET`, so `request_blank()`
// timed out and blanked unlocked even though every other link in the chain
// was by then correct. Resolving once turns the lock report into a single
// call.
//
// Safe to cache for this shell's lifetime: the seat's session only changes
// when greetd restarts, and that restarts the shell with it.
property string seatSessionPath: ""
Process {
id: seatPathProbe
running: true
command: ["sh", "-c",
"busctl get-property org.freedesktop.login1 " +
"/org/freedesktop/login1/seat/seat0 " +
"org.freedesktop.login1.Seat ActiveSession " +
"| grep -o '/org/freedesktop/login1/session/[^\"]*' " +
"|| busctl get-property org.freedesktop.login1 " +
"/org/freedesktop/login1/user/_$(id -u) " +
"org.freedesktop.login1.User Display " +
"| grep -o '/org/freedesktop/login1/session/[^\"]*'"]
stdout: StdioCollector {
onStreamFinished: {
root.seatSessionPath = text.trim();
console.log("[session] seat session path:", root.seatSessionPath);
// Replay: the lock may already be secure by the time this
// lands, and the edge that would have reported it is gone.
// Same fault `onHasLoginctlChanged` above exists to fix.
if (root.seatSessionPath !== "" && root.hasLoginctl)
lockedHintProc.report(root.locked);
}
}
}
Process {
id: lockedHintProc
property bool pending: false
property bool pendingValue: false
function report(value) {
if (running) {
// Coalesce: remember the latest value, replay on exit.
pending = true;
pendingValue = value;
return;
}
if (root.seatSessionPath === "") {
// The probe has not landed. Dropping here is safe only because
// the probe replays on completion — see its onStreamFinished.
return;
}
// Never `/session/auto`: that is the *caller's* session, and the
// shell is not in the one that owns the seat. Measured: viewtop in
// logind 66 (seat0/tty1), `qs` in 70 — the hint was being written to
// a session nobody reads while the graphical session stayed `no`
// forever. sessiond takes `locked` from `LockedHint` (doctrine §4),
// so that made `locked` permanently false and every blank went out
// on a session nobody could confirm was locked.
command = ["busctl", "call", "org.freedesktop.login1",
root.seatSessionPath,
"org.freedesktop.login1.Session",
"SetLockedHint", "b", value ? "true" : "false"];
running = true;
}
onExited: {
if (pending) {
pending = false;
report(pendingValue);
}
}
}
signal actionFailed(string action, int exitCode)
Process {
id: capabilityProbe
// Runs at construction: the probe must land before anything asks
// caps(), and every capability reads false until it does.
running: true
// One shell, one round trip. logind's Can* methods incorporate the
// policy and configuration that /sys/power/state cannot see (notably
// swap/resume setup for hibernation). Possible values include yes,
// no, challenge, and na; retain the value rather than flattening it.
// busctl prints `s "challenge"`; awk pulls the second field verbatim
// and the quotes come off in JS below. An earlier version parsed it
// with sed inside single quotes, where sh does not process the \" and
// sed ended up matching a literal backslash-quote that busctl never
// emits — so on the phone the probe returned nothing and every
// capability stuck at "unknown". Keep the shell here quote-free; do
// the string work in QML where there is no second escaping layer.
command: ["sh", "-c",
"command -v loginctl >/dev/null && echo loginctl; " +
"command -v systemctl >/dev/null && echo systemctl; " +
"if command -v busctl >/dev/null; then " +
"for cap in CanSuspend CanHibernate CanPowerOff CanReboot; do " +
"value=$(busctl --system call org.freedesktop.login1 /org/freedesktop/login1 " +
"org.freedesktop.login1.Manager $cap 2>/dev/null | awk '{print $2}'); " +
"[ -n \"$value\" ] && echo $cap=$value; " +
"done; " +
"fi; " +
"true"]
stdout: StdioCollector {
onStreamFinished: {
const lines = text.split("\n").map(l => l.trim());
root.hasLoginctl = lines.includes("loginctl");
root.hasSystemctl = lines.includes("systemctl");
const capability = (name) => {
const prefix = name + "=";
const line = lines.find(l => l.startsWith(prefix));
// Value arrives quoted from busctl (e.g. "challenge").
return line ? line.slice(prefix.length).replace(/"/g, "") : "unknown";
};
root.suspendCapability = capability("CanSuspend");
root.hibernateCapability = capability("CanHibernate");
root.poweroffCapability = capability("CanPowerOff");
root.rebootCapability = capability("CanReboot");
root.probed = true;
console.log("[session] capabilities:",
"loginctl=" + root.hasLoginctl,
"systemctl=" + root.hasSystemctl,
"suspend=" + root.suspendCapability,
"hibernate=" + root.hibernateCapability,
"poweroff=" + root.poweroffCapability,
"reboot=" + root.rebootCapability);
}
}
}
// --- Verb runner -------------------------------------------------------
// Every power verb goes through here so that none of them can fail
// silently. Upstream used execDetached, which cannot report an exit code.
Process {
id: verbProc
property string verb: ""
onExited: (exitCode, exitStatus) => {
root.lastAction = {
action: verbProc.verb,
status: exitCode === 0 ? "succeeded" : "failed",
exitCode: exitCode
};
if (exitCode !== 0) {
console.log(`[session] ${verbProc.verb} failed (exit ${exitCode})`);
root.actionFailed(verbProc.verb, exitCode);
}
}
}
function runVerb(verb, argv) {
// Process has one command slot. Overwriting it while a prior action
// is still running makes the eventual exit code belong to the wrong
// action, which is another form of silent failure.
if (verbProc.running) return false;
verbProc.verb = verb;
verbProc.command = argv;
root.lastAction = { action: verb, status: "running", exitCode: null };
verbProc.running = true;
return true;
}
// IPC returns when an action is accepted, not when the kernel has already
// suspended or powered off. This records the later Process outcome so a
// caller can distinguish "started" from "succeeded".
property var lastAction: ({ action: "", status: "idle", exitCode: null })
// systemctl owns the power verbs, NOT loginctl. loginctl only manages
// sessions/users/seats — `loginctl poweroff` exits 1 with "Unknown
// command verb" (verified on systemd 261, and its --help lists no power
// commands at all). Preferring loginctl here silently broke poweroff,
// reboot, suspend and hibernate from every shell surface: the button
// fired, the Process exited 1, and the phone stayed on (2026-07-20).
//
// The capability probe still asks logind over D-Bus (CanPowerOff etc.) —
// that part was always right; logind owns the *policy*. It just isn't
// the CLI that carries out the action.
function powerCommand(action) {
if (root.hasSystemctl) return ["systemctl", action];
// elogind ships loginctl with power verbs and usually no systemctl;
// only reachable on such a system, where these verbs do exist.
if (root.hasLoginctl) return ["loginctl", action];
return [];
}
// --- Inhibits ----------------------------------------------------------
// A bare "something is holding the machine awake" bool cannot answer the
// only question that matters when the phone will not sleep: WHAT is
// holding it, and why. Each holder gets a cookie and carries a reason.
property var inhibitors: ({})
property int nextCookie: 1
readonly property bool inhibited: Object.keys(root.inhibitors).length > 0
function inhibit(what, reason) {
const kind = String(what || "idle").trim().toLowerCase();
const why = String(reason || "").trim();
if (!why) return root.refuse("inhibit", "an inhibit must carry a reason");
// idle and sleep are wired today. idle uses the Wayland/hypridle
// mechanism via Idle.qml; sleep uses SessionEvents' delay-mode
// systemd-inhibit. Recording logout/user-switch without applying
// their mechanism would create a dangerous success-shaped no-op.
if (kind !== "idle" && kind !== "sleep")
return root.refuse("inhibit", `unsupported inhibit kind ${kind}; only idle and sleep are implemented`);
const cookie = String(root.nextCookie++);
// Reassign rather than mutate: QML only notifies on assignment, so an
// in-place insert would leave `inhibited` and any binding on it stale.
const next = Object.assign({}, root.inhibitors);
next[cookie] = { what: kind, reason: why };
root.inhibitors = next;
console.log(`[session] inhibit ${cookie}: ${kind} — ${why}`);
root.applyInhibits();
return { ok: true, cookie: cookie };
}
function uninhibit(cookie) {
if (!root.inhibitors[cookie])
return root.refuse("uninhibit", `no inhibitor with cookie ${cookie}`);
const next = Object.assign({}, root.inhibitors);
delete next[cookie];
root.inhibitors = next;
console.log(`[session] uninhibit ${cookie}`);
root.applyInhibits();
return { ok: true };
}
// Any holder inhibiting "idle" keeps the machine awake. Idle.qml owns the
// mechanism (it knows the hypridle quirk on this device); we own the
// policy of who is asking and why.
//
// "sleep" inhibitors are managed by SessionEvents (delay-mode
// systemd-inhibit). They don't need a mechanism toggle here —
// SessionEvents holds the inhibitor from startup and releases it
// only when PrepareForSleep(true) fires and the Wayland lock is secure.
function applyInhibits() {
const wantIdle = Object.values(root.inhibitors).some(i => i.what === "idle");
Idle.toggleInhibit(wantIdle);
}
// --- State -------------------------------------------------------------
// The projection an agent reads. Everything here is re-derived at call
// time; nothing is a bool we set ourselves and then trusted.
function state() {
const holders = Object.keys(root.inhibitors).map(c => ({
cookie: c,
what: root.inhibitors[c].what,
reason: root.inhibitors[c].reason
}));
return {
locked: root.locked,
lockRequested: GlobalStates.screenLocked,
idle: {
stage: IdleCoordinator.state,
nativeCoordinatorEnabled: IdleCoordinator.nativeEnabled
},
idleInhibited: Idle.inhibit,
inhibitors: holders,
lastAction: root.lastAction,
capabilities: root.caps(),
stepUp: typeof StepUpAuth !== "undefined" ? StepUpAuth.state() : null,
sleepInhibitorHeld: typeof SessionEvents !== "undefined" ? SessionEvents.sleepInhibitorHeld : null
};
}
// `probed` is not decoration: until the probe lands every capability reads
// false, and false-because-unknown is not the same claim as
// false-because-unsupported. A caller that ignores `probed` during the
// startup window would conclude this machine cannot suspend at all. Check
// `probed` before believing a false.
function caps() {
return {
probed: root.probed,
suspend: root.canSuspend,
suspendStatus: root.suspendCapability,
hibernate: root.canHibernate,
hibernateStatus: root.hibernateCapability,
poweroff: root.canPoweroff,
poweroffStatus: root.poweroffCapability,
reboot: root.canReboot,
rebootStatus: root.rebootCapability,
inhibitors: ["idle", "sleep"]
};
}
// --- Verbs -------------------------------------------------------------
// Every upstream ii verb is preserved by name and behavior, so existing
// call sites (LockScreen.qml's poweroff/reboot on the lock's power action,
// the session menus) keep working. What changed is that they now refuse
// honestly when the machine cannot do the thing, and log when it fails.
//
// Those call sites are all statements — `onClicked: Session.suspend()` —
// so they ignore the returned {ok, reason}. That is fine for the IPC
// caller, which reads the value, but it means a UI button that hits a
// refusal would otherwise do nothing at all, silently: press hibernate on
// the phone, no swap, nothing happens, no trace. Every refusal therefore
// goes through refuse(), which logs before it returns. A refused verb is
// an event, not a void.
function refuse(verb, reason) {
console.log(`[session] ${verb} refused: ${reason}`);
return { ok: false, reason: reason };
}
function closeAllWindows() {
HyprlandData.windowList.map(w => w.pid).forEach(pid => {
Quickshell.execDetached(["kill", pid]);
});
}
function pauseAllPlayers() {
for (const player of Mpris.players.values) {
if (player.canPause) player.pause();
}
}
function lock() {
// Raise our Wayland lock ourselves: logind's Lock signal is a request
// for session software to lock, not a Wayland lock implementation.
// We also notify logind when it is available so other consumers see
// the standard session event. The safe lock does not depend on that
// asynchronous notification returning successfully.
//
// Notify ONLY on the unlocked->locked edge. hypridle's lock_cmd
// fires on logind's Lock signal, so an unconditional
// `loginctl lock-session` here echoes back through logind ->
// hypridle -> this function forever. Observed on the phone: a
// sustained storm of lock requests (several per second, for hours)
// that re-locked the screen seconds after every unlock and chewed
// battery all night.
const alreadyLocked = GlobalStates.screenLocked;
GlobalStates.screenLocked = true;
if (alreadyLocked) {
return { ok: true, status: "already-locked" };
}
if (root.hasLoginctl) {
const notified = root.runVerb("lock", ["loginctl", "lock-session"]);
return notified
? { ok: true, status: "requested" }
: { ok: true, status: "requested", degraded: "logind notification skipped; another action is running" };
}
return { ok: true, degraded: "no loginctl; locked without logind" };
}
function unlock() {
// Deliberately not a verb an agent gets. Unlocking is the credential
// gate on this device — the only thing standing between a picked-up
// phone and the session. It is refused here so that no IPC caller can
// route around the PIN pad. The human unlocks; nothing else does.
return root.refuse("unlock", "unlock is the credential gate; not remotely callable");
}
function suspend() {
if (!root.probed) return root.refuse("suspend", "capabilities not probed yet");
if (!root.canSuspend) return root.refuse("suspend", "no loginctl or systemctl on this machine");
pauseAllPlayers();
if (!root.runVerb("suspend", root.powerCommand("suspend")))
return root.refuse("suspend", "another session action is still running");
return { ok: true, status: "started" };
}
function hibernate() {
if (!root.probed) return root.refuse("hibernate", "capabilities not probed yet");
// logind validates swap/resume configuration as well as kernel support,
// so a phone without hibernation refuses instead of firing a no-op.
if (!root.canHibernate)
return root.refuse("hibernate", "hibernate unavailable (logind: " + root.hibernateCapability + ")");
pauseAllPlayers();
if (!root.runVerb("hibernate", root.powerCommand("hibernate")))
return root.refuse("hibernate", "another session action is still running");
return { ok: true, status: "started" };
}
function poweroff() {
if (!root.probed) return root.refuse("poweroff", "capabilities not probed yet");
if (!root.canPoweroff) return root.refuse("poweroff", "no loginctl or systemctl on this machine");
closeAllWindows();
if (!root.runVerb("poweroff", root.powerCommand("poweroff")))
return root.refuse("poweroff", "another session action is still running");
return { ok: true, status: "started" };
}
function reboot() {
if (!root.probed) return root.refuse("reboot", "capabilities not probed yet");
if (!root.canReboot) return root.refuse("reboot", "no loginctl or systemctl on this machine");
closeAllWindows();
if (!root.runVerb("reboot", root.powerCommand("reboot")))
return root.refuse("reboot", "another session action is still running");
return { ok: true, status: "started" };
}
function rebootToFirmware() {
if (!root.hasSystemctl)
return root.refuse("rebootToFirmware", "firmware-setup reboot needs systemctl");
closeAllWindows();
if (!root.runVerb("rebootToFirmware", ["systemctl", "reboot", "--firmware-setup"]))
return root.refuse("rebootToFirmware", "another session action is still running");
return { ok: true, status: "started" };
}
function logout() {
closeAllWindows();
// loginctl terminate-session ends the session properly (logind tears
// down the scope and the seat); pkill Hyprland just kills the
// compositor and leaves logind believing the session is alive.
if (root.hasLoginctl) {
if (!root.runVerb("logout", ["loginctl", "terminate-session", ""]))
return root.refuse("logout", "another session action is still running");
return { ok: true, status: "started" };
}
if (!root.runVerb("logout", ["pkill", "-i", "Hyprland"]))
return root.refuse("logout", "another session action is still running");
return { ok: true, status: "started", degraded: "no loginctl; killed the compositor" };
}
function changePassword() {
Quickshell.execDetached(["bash", "-c", `${Config.options.apps.changePassword}`]);
}
function launchTaskManager() {
Quickshell.execDetached(["bash", "-c", `${Config.options.apps.taskManager}`]);
}
// NO IpcHandler here. This file used to register `target: "session"` with
// state/inhibit/uninhibit, and Lock.qml registers the same target with a
// strict superset (those three plus capabilities, lock, unlock, suspend,
// hibernate, poweroff, reboot, logout). Quickshell keeps whichever
// registers first and drops the other with a warning:
//
// QML IpcHandler at Session.qml[470:5]: Handler was registered but will
// not be used because another handler is registered for target session
//
// Which one won was load-order, not design — so `session inhibit` and
// `session uninhibit` were reachable or dead depending on the run, and
// deploy.sh's `session state` was riding the same coin flip. Since every
// verb here exists on Lock.qml's handler, the duplicate is removed rather
// than renamed: one target, one owner. Add new session verbs there.
// --- Boot-time IPC audit ------------------------------------------------
// Logs every IPC endpoint the shell exposes at startup. This is a
// defensive visibility measure — not a gate, not a refusal. It answers
// "what is reachable over the IPC socket?" so a human or an audit tool
// can verify the surface matches intent. The list is static per shell
// config; it does not change at runtime.
//
// Known IPC targets at time of writing:
// lock — LockScreen.qml (activate, focus)
// lock2 — Lock.qml (lock, unlock, toggleLock)
// dock — Dock.qml (launch, pin, unpin, moveStack, ...)
// sidebar — SidebarLeft.qml / SidebarRight.qml
// session — Lock.qml (the sole owner: state, capabilities, lock,
// unlock, suspend, hibernate, poweroff, reboot, logout,
// inhibit, uninhibit). Session.qml's duplicate was removed
// 2026-07-26; SessionScreen.qml's moved to `sessionMenu`.
// overview — Overview.qml
// keyboard — OnScreenKeyboard.qml
// appInventory — AppInventoryScope.qml
//
// If a new IpcHandler is added, this comment must be updated. The
// console.log below is the runtime check; the comment is the human
// audit trail.
// NOTE: Component.onCompleted doesn't work on Singletons in QML,
// and Timer isn't available in this module's import scope. Use a
// Process with onExited instead.
Process {
id: bootAuditProc
running: true
command: ["true"]
onExited: {
console.log("[session] boot IPC audit — shell exposes: "
+ "lock, lock2, dock, sidebar, session, overview, keyboard, appInventory");
}
}
}

View file

@ -0,0 +1,11 @@
singleton BitwiseFuzzy 1.0 BitwiseFuzzy.qml
singleton ColorUtils 1.0 ColorUtils.qml
singleton DateUtils 1.0 DateUtils.qml
singleton FileUtils 1.0 FileUtils.qml
singleton Fuzzy 1.0 Fuzzy.qml
singleton KeymapTranslation 1.0 KeymapTranslation.qml
singleton Levendist 1.0 Levendist.qml
singleton NotificationUtils 1.0 NotificationUtils.qml
singleton ObjectUtils 1.0 ObjectUtils.qml
singleton Session 1.0 Session.qml
singleton StringUtils 1.0 StringUtils.qml

View file

@ -0,0 +1,176 @@
import qs
import qs.services
import qs.modules.common
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pam
Scope {
id: root
enum ActionEnum { Unlock, Poweroff, Reboot }
signal shouldReFocus()
signal unlocked(targetAction: var)
signal failed()
// These properties are in the context and not individual lock surfaces
// so all surfaces can share the same state.
property string currentText: ""
property bool unlockInProgress: false
property bool showFailure: false
property bool fingerprintsConfigured: false
property var targetAction: LockContext.ActionEnum.Unlock
property bool alsoInhibitIdle: false
// FingerprintPreview owns the FPC pulse and hold state for both lock and
// step-up. This context only exposes it to the lock surface; it never
// decides whether a hold unlocks the session.
readonly property bool provisionalFingerprintEnabled:
FingerprintPreview.previewEnabled
readonly property int provisionalFingerprintHoldMs:
FingerprintPreview.holdMs
readonly property bool provisionalFingerprintHolding:
FingerprintPreview.holding && FingerprintPreview.activePurpose === "lock"
readonly property bool provisionalFingerprintConfirmed:
FingerprintPreview.confirmed && FingerprintPreview.confirmedPurpose === "lock"
readonly property bool provisionalFingerprintPulseSeen: FingerprintPreview.pulseSeen
readonly property real provisionalFingerprintHoldProgress:
FingerprintPreview.activePurpose === "lock" ? FingerprintPreview.holdProgress : 0
function resetTargetAction() {
root.targetAction = LockContext.ActionEnum.Unlock;
}
function clearText() {
root.currentText = "";
}
function resetClearTimer() {
passwordClearTimer.restart();
}
function reset() {
root.resetTargetAction();
root.clearText();
root.unlockInProgress = false;
stopFingerPam();
root.resetProvisionalFingerprint();
}
function beginProvisionalFingerprintHold() {
FingerprintPreview.beginHold("lock");
}
function cancelProvisionalFingerprintHold() {
FingerprintPreview.cancelHold("lock");
}
function confirmProvisionalFingerprintHold() {
FingerprintPreview.confirmHold("lock");
}
// Called by the diagnostic `fingerprint.signal` IPC seam. It reaches the
// same one-owner state as the root-owned FPC producer record.
function noteProvisionalFingerprintPulse() {
return FingerprintPreview.notePulse();
}
function resetProvisionalFingerprint() {
FingerprintPreview.reset("lock");
}
Timer {
id: passwordClearTimer
interval: 10000
onTriggered: {
root.reset();
}
}
onCurrentTextChanged: {
if (currentText.length > 0) {
showFailure = false;
GlobalStates.screenUnlockFailed = false;
}
GlobalStates.screenLockContainsCharacters = currentText.length > 0;
passwordClearTimer.restart();
}
function tryUnlock(alsoInhibitIdle = false) {
root.alsoInhibitIdle = alsoInhibitIdle;
root.unlockInProgress = true;
pam.start();
}
function tryFingerUnlock() {
if (root.fingerprintsConfigured) {
fingerPam.start();
}
}
function stopFingerPam() {
if (fingerPam.active) {
fingerPam.abort();
}
}
Process {
id: fingerprintCheckProc
running: true
command: ["bash", "-c", "fprintd-list $(whoami)"]
stdout: StdioCollector {
id: fingerprintOutputCollector
onStreamFinished: {
root.fingerprintsConfigured = fingerprintOutputCollector.text.includes("Fingerprints for user");
}
}
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) {
// console.warn("[LockContext] fprintd-list command exited with error:", exitCode, exitStatus);
root.fingerprintsConfigured = false;
}
}
}
PamContext {
id: pam
// pam_unix will ask for a response for the password prompt
onPamMessage: {
if (this.responseRequired) {
this.respond(root.currentText);
}
}
// pam_unix won't send any important messages so all we need is the completion status.
onCompleted: result => {
if (result == PamResult.Success) {
root.unlocked(root.targetAction);
stopFingerPam();
} else {
root.clearText();
root.unlockInProgress = false;
GlobalStates.screenUnlockFailed = true;
root.showFailure = true;
}
}
}
PamContext {
id: fingerPam
configDirectory: "pam"
config: "fprintd.conf"
onCompleted: result => {
if (result == PamResult.Success) {
root.unlocked(root.targetAction);
stopFingerPam();
} else if (result == PamResult.Error) { // if timeout or etc..
tryFingerUnlock()
}
}
}
}

View file

@ -0,0 +1,308 @@
// Souveraine patch to ii's stock LockScreen.qml.
//
// Two changes, both for phone duty where the lock is the only gate on the
// device:
// 1. The lock state is mirrored into Persistent.states.lock.locked, so a
// quickshell crash/restart while locked comes back locked instead of
// dropping the session lock on the floor.
// 2. initIfReady() also locks when the persisted flag says we died locked,
// not only on a fresh Hyprland instance.
// Everything else is unchanged stock ii.
pragma ComponentBehavior: Bound
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
Scope {
id: root
required property Component lockSurface
property alias context: lockContext
property Component sessionLockSurface: WlSessionLockSurface {
id: sessionLockSurface
color: "transparent"
Loader {
active: GlobalStates.screenLocked
anchors.fill: parent
opacity: active ? 1 : 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
sourceComponent: root.lockSurface
}
}
Process {
id: unlockKeyringProc
onExited: (exitCode, exitStatus) => {
KeyringStorage.fetchKeyringData();
}
}
// Set once, the first time the lock surface goes secure after boot, to
// dismiss the BootBloom overlay. The C splash is already gone by now (it
// released at the Act III handoff); nothing to poke here anymore.
property bool bootDismissed: false
function unlockKeyring() {
unlockKeyringProc.exec({
environment: ({
"UNLOCK_PASSWORD": lockContext.currentText
}),
command: ["bash", "-c", Quickshell.shellPath("scripts/keyring/unlock.sh")]
})
}
// This stores all the information shared between the lock surfaces on each screen.
// https://github.com/quickshell-mirror/quickshell-examples/tree/master/lockscreen
LockContext {
id: lockContext
Connections {
target: GlobalStates
function onScreenLockedChanged() {
Persistent.states.lock.locked = GlobalStates.screenLocked;
// Persistent is a file and answers asynchronously, so it can
// never be the thing a reload reads at construction. This is
// the same fact held in-process, where the reload can see it.
lockContinuity.held = GlobalStates.screenLocked;
if (GlobalStates.screenLocked) {
lockContext.reset();
lockContext.tryFingerUnlock();
}
}
}
onUnlocked: (targetAction) => {
// Perform the target action if it's not just unlocking
if (targetAction == LockContext.ActionEnum.Poweroff) {
Session.poweroff();
return;
} else if (targetAction == LockContext.ActionEnum.Reboot) {
Session.reboot();
return;
}
// Unlock the keyring if configured to do so
if (Config.options.lock.security.unlockKeyring) root.unlockKeyring(); // Async
// Unlock the screen before exiting, or the compositor will display a
// fallback lock you can't interact with.
GlobalStates.screenLocked = false;
// Reset
lockContext.reset();
// Post-unlock actions
if (lockContext.alsoInhibitIdle) {
lockContext.alsoInhibitIdle = false;
Idle.toggleInhibit(true);
}
}
}
// TASK-48. The lock REQUEST, carried across a quickshell scene reload by
// quickshell's own reload machinery — in-process and synchronous, which is
// what this has to be. Declared before the WlSessionLock on purpose:
// `Scope` is a ReloadPropagator and reloads its children in declaration
// order, so `held` is restored and applied before the lock below reloads.
//
// Why any of this is needed, from reading quickshell's session_lock.cpp:
// on reload the new WlSessionLock adopts the outgoing one's
// SessionLockManager and then calls realizeLockTarget() with whatever
// `locked` evaluated to at construction. GlobalStates is a fresh singleton
// by then, so screenLocked is false, so lockTarget is false — and the
// false branch is `unlock()`, which on an adopted manager that IS locked
// sends ext_session_lock_v1.unlock_and_destroy. A scene reload therefore
// UNLOCKS the session. What is left is a compositor with no lock surface,
// a phone that reads black, and `{"locked":false,"lockRequested":true}`.
// If instead the adoption does not match, the new manager's lock() is
// refused (the outgoing lock is still the process-global holder) and
// updateSurfaces(true) is called anyway — that is the FATAL this task was
// raised for. Same root cause, two faces.
//
// With the request true at construction, realizeLockTarget takes the adopt
// branch: surfaces are re-created against the SAME compositor lock,
// manager->lock() declines harmlessly because we already hold it, and
// updateSurfaces sees an active lock. No re-request, no denial, no
// unlock_and_destroy. This is the task's "adopting beats re-requesting",
// and it is the only branch that never opens the panel.
PersistentProperties {
id: lockContinuity
reloadableId: "souveraineLockContinuity"
property bool held: false
onLoaded: {
if (lockContinuity.held && !GlobalStates.screenLocked) {
console.log("[lock] scene reload owed a lock — adopting, not re-requesting");
GlobalStates.screenLocked = true;
}
}
}
WlSessionLock {
id: lock
// Explicit so the adoption survives being reparented out of a Scope.
// Under a Scope children match by index and this is unused; anywhere
// else an empty reloadableId means oldInstance is ALWAYS null, the
// manager is never adopted, and the lock cannot be re-acquired for the
// life of the process.
reloadableId: "souveraineSessionLock"
locked: GlobalStates.screenLocked
surface: root.sessionLockSurface
// `secure` is the compositor's acknowledgement that a real
// session-lock surface is up — not our requested bool. Keep it
// separate so an IPC caller cannot mistake a queued lock for a secure
// surface when it is deciding whether personal content may be exposed.
// It is also the true "lockscreen is ready" event: the one moment we
// dismiss the boot bloom. The C splash released DRM master back at the
// Act III→IV handoff (~6.8s); the quickshell BootBloom overlay has
// covered all of Hyprland's startup since. Now our own lock surface is
// mapped and secure, so fade the bloom out to reveal it. Firing on the
// request edge (screenLocked) was too early — the lock surface wasn't
// on screen yet. bootDismissed keeps it once-only so re-locks after
// unlock never re-hide a bloom that's already gone.
onSecureChanged: {
GlobalStates.screenLockSecure = secure;
console.log("[lock] session lock secure=" + secure);
root.dismissBloomIfSecure();
}
// The dismissal above is an EDGE, and a scene reload is exactly the
// case where the edge is in the past. `GlobalStates.bootBloomActive`
// defaults to true on every scene construction and `bootDismissed`
// resets with it, but a reload during an already-secure lock never
// moves `secure` — so nothing ever cleared the bloom and the phone sat
// under a full-screen white overlay until the shell was restarted.
// Observed 2026-07-29: a reload at 10:44:10 with no secure transition
// after it, and a solid white `grim` capture.
//
// Same shape as the locked_ack edge that never re-fired after a
// sessiond restart, and as the ChargeRate stale-scene reload. Check the
// LEVEL at construction as well as the edge.
Component.onCompleted: root.dismissBloomIfSecure()
// The compositor can end our lock without us asking: ext-session-lock
// `finished` (denied because another client held it) makes quickshell
// drop `locked` to false C++-side. Our request bool never hears about
// it, so it lingers true — which lies to every gate reading it
// (redaction, capability tiers, session state IPC) and blocks
// re-locking, because the `locked:` binding only re-fires on a
// false->true edge of screenLocked. Resync on that path. A normal
// unlock clears screenLocked *before* the binding drops `locked`,
// so this guard stays quiet there.
onLockStateChanged: {
if (!lock.locked && GlobalStates.screenLocked) {
console.log("[lock] compositor ended our session lock while still requested — resyncing");
GlobalStates.screenLocked = false;
}
}
}
// `secure` is an EDGE, and after a reload that adopts the lock that edge
// is in the past: the compositor acked before this tree existed, so
// onSecureChanged never fires and GlobalStates.screenLockSecure would sit
// false on a session that is genuinely secure. Everything gating on it —
// redaction, capability tiers, and the locked_ack this handoff owes
// sessiond — would then be wrong in the dangerous direction.
//
// Publish the LEVEL once the reload has actually run. Component.onCompleted
// is too early: Reloadable defers onReload to after component completion,
// so `lock.secure` is still false there. Setting screenLockSecure is enough
// to send the ack — SessiondBridge is already listening for it.
Connections {
target: Quickshell
function onReloadCompleted() {
GlobalStates.screenLockSecure = lock.secure;
root.dismissBloomIfSecure();
}
}
// Idempotent, and deliberately NOT a timeout. A bloom that outlives its
// reason is a bug to locate, not something to paper over with a timer — if
// this is still up while the lock is secure, the caller is missing and the
// fix belongs where the call is missing.
function dismissBloomIfSecure() {
if (!lock.secure || root.bootDismissed)
return;
root.bootDismissed = true;
GlobalStates.bootBloomActive = false;
}
function lock() {
if (Config.options.lock.useHyprlock) {
Quickshell.execDetached(["bash", "-c", "pidof hyprlock || hyprlock"]);
return;
}
GlobalStates.screenLocked = true;
}
IpcHandler {
target: "lock"
function activate(): void {
root.lock();
}
function focus(): void {
lockContext.shouldReFocus();
}
}
GlobalShortcut {
name: "lock"
description: "Locks the screen"
onPressed: {
root.lock()
}
}
GlobalShortcut {
name: "lockFocus"
description: "Re-focuses the lock screen. This is because Hyprland after waking up for whatever reason"
+ "decides to keyboard-unfocus the lock screen"
onPressed: {
lockContext.shouldReFocus();
}
}
property bool initDone: false
function initIfReady() {
if (!Config.ready || !Persistent.ready || root.initDone) return;
root.initDone = true;
// Register with sessiond (and start the heartbeat) before deciding
// the startup lock state. If sessiond holds the session lock, it
// releases on our shell_ready and we MUST lock immediately — the
// compositor is holding an abandoned lock for us to inherit
// (misc:allow_session_lock_restore). No sessiond = cb(false) and
// the legacy launchOnStartup rules decide alone.
SessiondBridge.shellReady(function(mustLock) {
if (mustLock
|| (Config.options.lock.launchOnStartup
&& (Persistent.isNewHyprlandInstance || Persistent.states.lock.locked))) {
root.lock();
} else {
KeyringStorage.fetchKeyringData();
}
});
}
Connections {
target: Config
function onReadyChanged() {
root.initIfReady();
}
}
Connections {
target: Persistent
function onReadyChanged() {
root.initIfReady();
}
}
}

View file

@ -0,0 +1,2 @@
LockContext 1.0 LockContext.qml
LockScreen 1.0 LockScreen.qml

View file

@ -0,0 +1,7 @@
singleton Appearance 1.0 Appearance.qml
singleton Config 1.0 Config.qml
singleton Directories 1.0 Directories.qml
singleton Icons 1.0 Icons.qml
singleton Images 1.0 Images.qml
singleton Persistent 1.0 Persistent.qml
ShellModel 1.0 ShellModel.qml

View file

@ -0,0 +1,43 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
import qs.modules.common.widgets
// Souveraine: responsive variant of ii's ContentPage. Upstream clamps the
// content column to baseWidth (600) and centers it — fine on a desktop
// window, but on a narrow/touch surface it clips both edges and stretches a
// huge gap into every row. Here, when the page is narrower than 650px the
// column is anchored left+right with fixed margins, so it tracks the live
// page width (reacts to fullscreen/resize) and nothing clips. Desktop
// windows (>=650px) keep upstream's clamp-and-center behavior.
StyledFlickable {
id: root
property real baseWidth: 600
property bool forceWidth: false
property real bottomContentPadding: 100
readonly property bool narrow: root.width < 650
readonly property real sideMargin: 20
default property alias contentData: contentColumn.data
clip: true
contentHeight: contentColumn.implicitHeight + root.bottomContentPadding
implicitWidth: contentColumn.implicitWidth
ColumnLayout {
id: contentColumn
spacing: 30
// Narrow: anchor to both edges so the column IS the page width minus
// symmetric margins — tracks resize, no clipping, no centered-overflow.
// Wide: fixed width, centered (upstream behavior).
anchors {
top: parent.top
margins: root.sideMargin
horizontalCenter: root.narrow ? undefined : parent.horizontalCenter
left: root.narrow ? parent.left : undefined
right: root.narrow ? parent.right : undefined
}
width: root.narrow ? (root.width - root.sideMargin * 2)
: (root.forceWidth ? root.baseWidth : Math.max(root.baseWidth, implicitWidth))
}
}

View file

@ -0,0 +1,78 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
import qs.modules.common.widgets
/**
* Material 3 FAB.
*
* Souveraine override of ii's widget: the label inside the collapsed Revealer
* centers on the button instead of the Revealer (see the comment at
* buttonText), breaking the implicitHeight binding loop upstream logs on every
* startup. Everything else is verbatim ii — diff against ii-base before
* re-applying if ii updates.
*/
RippleButton {
id: root
property string iconText: "add"
property bool expanded: false
property real baseSize: 56
property real elementSpacing: 5
implicitWidth: expanded ? (Math.max(contentRowLayout.implicitWidth + 10 * 2, baseSize)) : baseSize
implicitHeight: baseSize
buttonRadius: baseSize / 14 * 4
colBackground: Appearance.colors.colPrimaryContainer
colBackgroundHover: Appearance.colors.colPrimaryContainerHover
colRipple: Appearance.colors.colPrimaryContainerActive
property color colOnBackground: Appearance.colors.colOnPrimaryContainer
contentItem: Row {
id: contentRowLayout
property real horizontalMargins: (root.baseSize - icon.width) / 2
anchors {
verticalCenter: parent?.verticalCenter
left: parent?.left
leftMargin: contentRowLayout.horizontalMargins
}
spacing: 0
MaterialSymbol {
id: icon
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
iconSize: 26
color: root.colOnBackground
text: root.iconText
}
Loader {
anchors.verticalCenter: parent.verticalCenter
visible: root.buttonText?.length > 0
active: true
sourceComponent: Revealer {
visible: root.expanded || implicitWidth > 0
reveal: root.expanded
implicitWidth: reveal ? (buttonText.implicitWidth + root.elementSpacing + contentRowLayout.horizontalMargins) : 0
StyledText {
id: buttonText
anchors {
left: parent.left
leftMargin: root.elementSpacing
// Center on the BUTTON, not the Revealer (2026-08-05).
// Centering on the Revealer bound the text's y to the
// Revealer's height, whose implicitHeight is
// childrenRect.height, which depends on the text's
// y — the implicitHeight binding loop logged on every
// startup (FloatingActionButton.qml[45:30]). The
// Revealer and the button share a center anyway, so
// this is visually identical and acyclic.
verticalCenter: root.verticalCenter
}
text: root.buttonText
color: Appearance.colors.colOnPrimaryContainer
font.pixelSize: 14
font.weight: 450
}
}
}
}
}

View file

@ -0,0 +1,94 @@
// Souveraine override of ii's FullscreenPolkitWindow.
//
// One change: on a phone the prompt takes keyboard focus exclusively.
//
// Upstream uses WlrKeyboardFocus.OnDemand, which is right for a desktop —
// the prompt is one surface among many and focus follows the pointer. On the
// phone it made the password field untypable in a way that looked like a
// keyboard bug and wasn't. Verified live 2026-07-26: the on-screen keyboard
// rose with the prompt and its keys went nowhere.
//
// The chain: squeekboard delivers keystrokes through the input-method
// protocol to whatever surface holds keyboard focus, and Qt only activates a
// text-input context on a surface that HAS that focus. With OnDemand a
// layer-shell surface is granted focus by a click — but the field the user
// needs to click is inside the very surface that has no context yet, and
// there is no pointer on a phone to hover one into existence. Nothing ever
// activated, so the keys had no destination.
//
// Exclusive is also simply what this surface is: a modal authorization
// prompt on the Overlay layer, the same posture the lock surface takes. Esc
// and the cancel button both still dismiss it (see PolkitContent), so the
// grab is not a trap.
//
// Desktop keeps OnDemand — there is a hardware keyboard there and no reason
// to change a working path.
pragma ComponentBehavior: Bound
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
import Quickshell
import Quickshell.Wayland
Scope {
id: root
required property Component contentComponent
readonly property bool phone: Config.options?.souveraine?.phone ?? false
Loader {
active: PolkitService.active
sourceComponent: Variants {
model: Quickshell.screens
delegate: PanelWindow {
id: panelWindow
required property var modelData
screen: modelData
// Do not cover the on-screen keyboard. squeekboard is a
// layer-shell surface on `top`; this prompt is on `overlay`,
// so a full-screen prompt sits above it. The dialog is
// transparent, which made that look survivable — the keyboard
// was plainly visible through it — but this surface owned the
// whole screen's input region, so every tap in the bottom
// third landed on the dialog and the keys never saw a touch.
// Visible and inert, exactly as reported.
//
// Measured on device: osk is 0 732 540 348 on a 540x1080
// panel, so the keyboard is the bottom ~32%. While it is up,
// stop anchoring the bottom edge and end the surface above it.
readonly property bool yieldToOsk: root.phone && GlobalStates.oskOpen
anchors {
top: true
left: true
right: true
bottom: !panelWindow.yieldToOsk
}
// Only consulted when the bottom anchor is released.
implicitHeight: panelWindow.yieldToOsk
? Math.round((panelWindow.screen?.height ?? 1080) * 0.65)
: 0
color: "transparent"
WlrLayershell.namespace: "quickshell:polkit"
// Exclusive was tried on 2026-07-26 and made it worse: with an
// exclusive layer grab the on-screen keyboard stopped taking
// touch at all — keys did not even highlight. OnDemand is what
// the working text fields elsewhere in this shell use, and
// squeekboard follows them fine.
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
WlrLayershell.layer: WlrLayer.Overlay
exclusionMode: ExclusionMode.Ignore
Loader {
anchors.fill: parent
sourceComponent: root.contentComponent
}
}
}
}
}

View file

@ -0,0 +1,41 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
// Souveraine: tooltip-trigger fix. Upstream treated a parent with no
// `hovered` property (parent.hovered === undefined) as "always show" — so any
// StyledToolTip attached to a non-hoverable parent (e.g. ConfigSpinBox, a
// RowLayout) was permanently visible. That's latent on desktop and broken on
// touch. Now: a tooltip only shows on a real positive hover (hoverable parent
// actually hovered) or the explicit alternativeVisibleCondition. Non-hoverable
// parents default off.
ToolTip {
id: root
property bool extraVisibleCondition: true
property bool alternativeVisibleCondition: false
readonly property bool internalVisibleCondition: (extraVisibleCondition && (parent?.hovered ?? false)) || alternativeVisibleCondition
verticalPadding: 5
horizontalPadding: 10
background: null
font {
family: Appearance.font.family.main
variableAxes: Appearance.font.variableAxes.main
pixelSize: Appearance?.font.pixelSize.smaller ?? 14
hintingPreference: Font.PreferNoHinting // Prevent shaky text
}
delay: 0
visible: internalVisibleCondition
contentItem: StyledToolTipContent {
id: contentItem
font: root.font
text: root.text
shown: root.internalVisibleCondition
horizontalPadding: root.horizontalPadding
verticalPadding: root.verticalPadding
}
}

View file

@ -0,0 +1,111 @@
AddressBar 1.0 AddressBar.qml
AddressBreadcrumb 1.0 AddressBreadcrumb.qml
ButtonGroup 1.0 ButtonGroup.qml
CalendarView 1.0 CalendarView.qml
Circle 1.0 Circle.qml
CircularProgress 1.0 CircularProgress.qml
CliphistImage 1.0 CliphistImage.qml
ClippedFilledCircularProgress 1.0 ClippedFilledCircularProgress.qml
ClippedProgressBar 1.0 ClippedProgressBar.qml
ConfigRow 1.0 ConfigRow.qml
ConfigSelectionArray 1.0 ConfigSelectionArray.qml
ConfigSlider 1.0 ConfigSlider.qml
ConfigSpinBox 1.0 ConfigSpinBox.qml
ConfigSwitch 1.0 ConfigSwitch.qml
ContentPage 1.0 ContentPage.qml
ContentSection 1.0 ContentSection.qml
ContentSubsection 1.0 ContentSubsection.qml
ContentSubsectionLabel 1.0 ContentSubsectionLabel.qml
CustomIcon 1.0 CustomIcon.qml
DashedBorder 1.0 DashedBorder.qml
DialogButton 1.0 DialogButton.qml
DialogListItem 1.0 DialogListItem.qml
DirectoryIcon 1.0 DirectoryIcon.qml
DragManager 1.0 DragManager.qml
ErrorShakeAnimation 1.0 ErrorShakeAnimation.qml
FadeLoader 1.0 FadeLoader.qml
Favicon 1.0 Favicon.qml
FloatingActionButton 1.0 FloatingActionButton.qml
FlowButtonGroup 1.0 FlowButtonGroup.qml
FocusedScrollMouseArea 1.0 FocusedScrollMouseArea.qml
FullscreenPolkitWindow 1.0 FullscreenPolkitWindow.qml
Graph 1.0 Graph.qml
GroupButton 1.0 GroupButton.qml
IconAndTextToolbarButton 1.0 IconAndTextToolbarButton.qml
IconToolbarButton 1.0 IconToolbarButton.qml
KeyboardKey 1.0 KeyboardKey.qml
LightDarkPreferenceButton 1.0 LightDarkPreferenceButton.qml
MaskMultiEffect 1.0 MaskMultiEffect.qml
MaterialCookie 1.0 MaterialCookie.qml
MaterialLoadingIndicator 1.0 MaterialLoadingIndicator.qml
MaterialShape 1.0 MaterialShape.qml
MaterialShapeWrappedMaterialSymbol 1.0 MaterialShapeWrappedMaterialSymbol.qml
MaterialSymbol 1.0 MaterialSymbol.qml
MaterialTextArea 1.0 MaterialTextArea.qml
MaterialTextField 1.0 MaterialTextField.qml
MenuButton 1.0 MenuButton.qml
NavigationRail 1.0 NavigationRail.qml
NavigationRailButton 1.0 NavigationRailButton.qml
NavigationRailExpandButton 1.0 NavigationRailExpandButton.qml
NavigationRailTabArray 1.0 NavigationRailTabArray.qml
NoticeBox 1.0 NoticeBox.qml
NotificationActionButton 1.0 NotificationActionButton.qml
NotificationAppIcon 1.0 NotificationAppIcon.qml
NotificationGroup 1.0 NotificationGroup.qml
NotificationGroupExpandButton 1.0 NotificationGroupExpandButton.qml
NotificationItem 1.0 NotificationItem.qml
NotificationListView 1.0 NotificationListView.qml
OptionalMaterialSymbol 1.0 OptionalMaterialSymbol.qml
PagePlaceholder 1.0 PagePlaceholder.qml
PointingHandInteraction 1.0 PointingHandInteraction.qml
PointingHandLinkHover 1.0 PointingHandLinkHover.qml
PopupToolTip 1.0 PopupToolTip.qml
Revealer 1.0 Revealer.qml
RippleButton 1.0 RippleButton.qml
RippleButtonWithIcon 1.0 RippleButtonWithIcon.qml
RoundCorner 1.0 RoundCorner.qml
ScrollEdgeFade 1.0 ScrollEdgeFade.qml
SecondaryTabBar 1.0 SecondaryTabBar.qml
SecondaryTabButton 1.0 SecondaryTabButton.qml
SelectionDialog 1.0 SelectionDialog.qml
SelectionGroupButton 1.0 SelectionGroupButton.qml
SineCookie 1.0 SineCookie.qml
SqueezedAnnotationStyledText 1.0 SqueezedAnnotationStyledText.qml
StyledBlurEffect 1.0 StyledBlurEffect.qml
StyledComboBox 1.0 StyledComboBox.qml
StyledDropShadow 1.0 StyledDropShadow.qml
StyledFlickable 1.0 StyledFlickable.qml
StyledImage 1.0 StyledImage.qml
StyledIndeterminateProgressBar 1.0 StyledIndeterminateProgressBar.qml
StyledListView 1.0 StyledListView.qml
StyledProgressBar 1.0 StyledProgressBar.qml
StyledRadioButton 1.0 StyledRadioButton.qml
StyledRectangularShadow 1.0 StyledRectangularShadow.qml
StyledScrollBar 1.0 StyledScrollBar.qml
StyledSlider 1.0 StyledSlider.qml
StyledSpinBox 1.0 StyledSpinBox.qml
StyledSwitch 1.0 StyledSwitch.qml
StyledText 1.0 StyledText.qml
StyledTextArea 1.0 StyledTextArea.qml
StyledTextInput 1.0 StyledTextInput.qml
StyledToolTip 1.0 StyledToolTip.qml
StyledToolTipContent 1.0 StyledToolTipContent.qml
ThumbnailImage 1.0 ThumbnailImage.qml
Toolbar 1.0 Toolbar.qml
ToolbarButton 1.0 ToolbarButton.qml
ToolbarPairedFab 1.0 ToolbarPairedFab.qml
ToolbarTabBar 1.0 ToolbarTabBar.qml
ToolbarTabButton 1.0 ToolbarTabButton.qml
ToolbarTextField 1.0 ToolbarTextField.qml
VerticalButtonGroup 1.0 VerticalButtonGroup.qml
VibrantToolbarButton 1.0 VibrantToolbarButton.qml
WaveVisualizer 1.0 WaveVisualizer.qml
WavyLine 1.0 WavyLine.qml
WeekRow 1.0 WeekRow.qml
WindowDialog 1.0 WindowDialog.qml
WindowDialogButtonRow 1.0 WindowDialogButtonRow.qml
WindowDialogParagraph 1.0 WindowDialogParagraph.qml
WindowDialogSectionHeader 1.0 WindowDialogSectionHeader.qml
WindowDialogSeparator 1.0 WindowDialogSeparator.qml
WindowDialogSlider 1.0 WindowDialogSlider.qml
WindowDialogTitle 1.0 WindowDialogTitle.qml