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,254 @@
import QtQuick
import QtQuick.Layouts
import qs.services
import qs.modules.common
import qs.modules.common.widgets
// Device — Souveraine form-factor and (later) per-device overrides.
// Principle: pages are views over Config.options / the owning daemon;
// nothing app-private. This page holds the knobs that describe WHAT this
// device is, so behaviors elsewhere gate on config, not hardcoded checks.
ContentPage {
forceWidth: true
ContentSection {
icon: "smartphone"
title: Translation.tr("Form factor")
ConfigSwitch {
buttonIcon: "smartphone"
text: Translation.tr("Phone mode")
checked: Config.options.souveraine.phone
onCheckedChanged: {
Config.options.souveraine.phone = checked;
}
StyledToolTip {
text: Translation.tr("Gates phone behaviors: OSK rises for polkit prompts, phone-only pages, single-column layouts. Laptop deploys leave this off.")
}
}
}
// ── Proprioception ───────────────────────────────────────────────────
// TASK-08(f) / TASK-19: the state machine computes its state, its
// evidence, its confidence and its per-source health, and until now none
// of it reached a screen. `forensic.jsonl` knew; the device could not tell
// you. A body that cannot feel itself is the thing this OS is not
// supposed to be.
//
// READOUT ONLY, deliberately. TASK-19: the confidence gates are computed,
// logged and never branched on, so a control over them "would be lying" —
// showing a threshold slider nothing consults breaks this page's own rule
// against success-shaped switches. Observations can be shown honestly
// today; controls wait on TASK-08(g).
property bool _watchingEvidence: false
function _startWatching() {
if (_watchingEvidence) return;
_watchingEvidence = true;
DeviceEvidence.watch();
}
function _stopWatching() {
if (!_watchingEvidence) return;
_watchingEvidence = false;
DeviceEvidence.unwatch();
}
Component.onCompleted: _startWatching()
Component.onDestruction: _stopWatching()
ContentSection {
icon: "monitor_heart"
title: Translation.tr("Device state")
// The laptop has no sessiond. Say so, rather than rendering zeroes
// that look like a healthy reading (§10: "no evidence" and "evidence
// says nothing is happening" must not be the same state).
StyledText {
Layout.fillWidth: true
visible: !DeviceEvidence.available
text: Translation.tr("sessiond is not answering on this device — no state to report.")
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
wrapMode: Text.WordWrap
}
Repeater {
model: DeviceEvidence.available ? [
{ k: Translation.tr("State"), v: DeviceEvidence.state.device_state ?? "—" },
{ k: Translation.tr("Lock phase"), v: DeviceEvidence.state.phase ?? "—" },
{ k: Translation.tr("Locked"), v: (DeviceEvidence.state.locked ?? false) ? Translation.tr("yes") : Translation.tr("no") },
{ k: Translation.tr("Panel"), v: (DeviceEvidence.state.panel_on ?? false) ? Translation.tr("on") : Translation.tr("off") },
{ k: Translation.tr("Dimmed"), v: (DeviceEvidence.state.dimmed ?? false) ? Translation.tr("yes") : Translation.tr("no") },
{ k: Translation.tr("Display active"), v: (DeviceEvidence.state.display_active ?? false) ? Translation.tr("yes") : Translation.tr("no") },
{ k: Translation.tr("Idle"), v: (DeviceEvidence.state.idle_secs ?? 0) + "s" },
{ k: Translation.tr("Observed"), v: (DeviceEvidence.state.observed ?? false) ? Translation.tr("yes") : Translation.tr("no") },
{ k: Translation.tr("Confidence"), v: Number(DeviceEvidence.state.observed_confidence ?? 0).toFixed(2) },
{ k: Translation.tr("Wake suppressed"), v: (DeviceEvidence.state.suppress_dpms_wake ?? false) ? Translation.tr("yes") : Translation.tr("no") },
{ k: Translation.tr("Shell alive"), v: (DeviceEvidence.state.shell_alive ?? false) ? Translation.tr("yes") : Translation.tr("no") }
] : []
delegate: RowLayout {
required property var modelData
Layout.fillWidth: true
spacing: 8
StyledText {
Layout.fillWidth: true
text: modelData.k
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
}
StyledText {
text: String(modelData.v)
color: Appearance.colors.colOnLayer1
font.pixelSize: Appearance.font.pixelSize.smaller
}
}
}
}
ContentSection {
icon: "sensors"
title: Translation.tr("Evidence sources")
// The flag §10 was built for. It rides every forensic snapshot and had
// nowhere to appear: the SLPI outage on 2026-07-25 killed every sensor
// for four hours and exited status 0, so the crash reporter
// structurally could not help.
StyledText {
Layout.fillWidth: true
visible: DeviceEvidence.available && (DeviceEvidence.state.sensors_degraded ?? false)
text: Translation.tr("A source reported and then went silent. Readings below are not trustworthy.")
color: Appearance.colors.colError
font.pixelSize: Appearance.font.pixelSize.smaller
wrapMode: Text.WordWrap
}
StyledText {
Layout.fillWidth: true
visible: DeviceEvidence.available
text: Translation.tr("live = reporting · unknown = never heard from (no reporter wired) · down = spoke, then stopped")
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
wrapMode: Text.WordWrap
}
Repeater {
model: {
if (!DeviceEvidence.available) return [];
const health = DeviceEvidence.state.sensor_health ?? {};
const fresh = DeviceEvidence.state.evidence_fresh ?? {};
return Object.keys(health).map(name => ({
name: name,
health: health[name],
fresh: fresh[name] === true
}));
}
delegate: RowLayout {
required property var modelData
Layout.fillWidth: true
spacing: 8
MaterialSymbol {
iconSize: Appearance.font.pixelSize.normal
text: modelData.health === "live" ? "sensors"
: modelData.health === "down" ? "sensors_off"
: "help"
color: modelData.health === "down" ? Appearance.colors.colError
: modelData.health === "live" ? Appearance.colors.colOnLayer1
: Appearance.colors.colSubtext
}
StyledText {
Layout.fillWidth: true
text: modelData.name
color: Appearance.colors.colOnLayer1
font.pixelSize: Appearance.font.pixelSize.smaller
}
StyledText {
// "unknown" is not a failure — accel, light and touch have
// no reporter on this device and correctly sit there
// forever. Only a source that spoke and then stopped failed.
text: modelData.health + (modelData.fresh ? Translation.tr(" · fresh") : "")
color: modelData.health === "down" ? Appearance.colors.colError
: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
}
}
}
}
ContentSection {
icon: "history"
title: Translation.tr("Recent decisions")
StyledText {
Layout.fillWidth: true
text: Translation.tr("What the machine last decided, and what it decided it from. The same entries the forensic trail hash-chains.")
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
wrapMode: Text.WordWrap
}
Repeater {
// Newest first, by seq — independent of the order the buffer
// happens to return.
model: (DeviceEvidence.recentDecisions ?? []).slice()
.sort((a, b) => (b.seq ?? 0) - (a.seq ?? 0))
.slice(0, 12)
delegate: ColumnLayout {
required property var modelData
Layout.fillWidth: true
spacing: 1
RowLayout {
Layout.fillWidth: true
spacing: 6
StyledText {
text: {
// `event` is a tagged union (decision,
// state-transition, sensor-input, error-*). Take
// whichever key it carries rather than assuming.
const ev = modelData.event ?? {};
const kind = Object.keys(ev)[0] ?? "event";
const body = ev[kind] ?? {};
return body.decision ?? body.to ?? kind;
}
color: Appearance.colors.colOnLayer1
font.pixelSize: Appearance.font.pixelSize.smaller
font.bold: true
}
Item { Layout.fillWidth: true }
StyledText {
text: "#" + (modelData.seq ?? "?")
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
}
}
StyledText {
Layout.fillWidth: true
visible: (modelData.reason ?? "").length > 0
text: modelData.reason ?? ""
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
wrapMode: Text.WordWrap
}
}
}
}
ContentSection {
icon: "tune"
title: Translation.tr("Device overrides")
StyledText {
Layout.fillWidth: true
text: Translation.tr("Per-device profile overrides (panel size, sensor set, feel presets) land here as the framework grows. One config tree, many devices.")
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
wrapMode: Text.WordWrap
}
}
}

View file

@ -0,0 +1,71 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.services
import qs.modules.common
import qs.modules.common.widgets
ContentPage {
id: page
forceWidth: true
readonly property var monitor: Brightness.monitors.length > 0
? Brightness.monitors[0] : null
ContentSection {
icon: "brightness_6"
title: Translation.tr("Brightness")
RowLayout {
Layout.fillWidth: true
spacing: 12
MaterialSymbol {
text: "brightness_low"
iconSize: 22
}
StyledSlider {
Layout.fillWidth: true
enabled: page.monitor ? page.monitor.ready : false
value: page.monitor ? page.monitor.brightness : 0
from: 0.01
to: 1
onMoved: {
if (page.monitor) page.monitor.setBrightness(value);
}
}
StyledText {
text: page.monitor && page.monitor.ready
? `${Math.round(page.monitor.brightness * 100)}%` : "—"
font.family: Appearance.font.family.numbers
}
}
ConfigSwitch {
buttonIcon: "flash_off"
text: Translation.tr("Anti-flashbang dimming")
checked: Config.options.light.antiFlashbang.enable
onCheckedChanged: Config.options.light.antiFlashbang.enable = checked
StyledToolTip {
text: Translation.tr("Temporarily softens large brightness jumps when content changes.")
}
}
}
ContentSection {
icon: "monitor"
title: Translation.tr("Built-in display")
StyledText {
Layout.fillWidth: true
text: Quickshell.screens.length > 0
? Translation.tr("%1 · %2 × %3 logical pixels")
.arg(Quickshell.screens[0].name)
.arg(Quickshell.screens[0].width)
.arg(Quickshell.screens[0].height)
: Translation.tr("No display reported")
color: Appearance.colors.colSubtext
wrapMode: Text.WordWrap
}
}
}

View file

@ -0,0 +1,331 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import qs.services
import qs.modules.common
import qs.modules.common.widgets
// Dock — every configurable the dock components read. Feel knobs are the
// single source (components read Config, never hardcode); stacks and pins
// are managed by drag on the dock itself — an editor lands here later.
ContentPage {
forceWidth: true
ContentSection {
icon: "dock_to_bottom"
title: Translation.tr("Behavior")
ConfigSwitch {
buttonIcon: "dock_to_bottom"
text: Translation.tr("Enable dock")
checked: Config.options.dock.enable
onCheckedChanged: {
Config.options.dock.enable = checked;
}
StyledToolTip {
text: Translation.tr("The dock surface itself. This settings app runs standalone, so it can always re-enable it.")
}
}
ConfigSwitch {
buttonIcon: "bottom_panel_open"
text: Translation.tr("Auto-hide on Home")
checked: Config.options.dock.autoHide
onCheckedChanged: {
Config.options.dock.autoHide = checked;
}
StyledToolTip {
text: Translation.tr("Hide the Home dock until the pointer reaches the bottom edge.")
}
}
ConfigSwitch {
buttonIcon: "push_pin"
text: Translation.tr("Reserve screen space")
checked: Config.options.dock.pinnedOnStartup
enabled: !Config.options.dock.autoHide
onCheckedChanged: {
Config.options.dock.pinnedOnStartup = checked;
}
StyledToolTip {
text: Translation.tr("Keep normal windows above the visible dock instead of allowing them behind it.")
}
}
ConfigSwitch {
buttonIcon: "swipe_up"
text: Translation.tr("Reveal from other zones")
checked: Config.options.dock.hoverToReveal
onCheckedChanged: {
Config.options.dock.hoverToReveal = checked;
}
StyledToolTip {
text: Translation.tr("Keep a bottom-edge reveal strip available when the dock is not Home furniture.")
}
}
ConfigSpinBox {
icon: "hourglass_top"
text: Translation.tr("Reveal delay (ms)")
value: Config.options.dock.revealDelayMs
from: 0
to: 1000
stepSize: 25
onValueChanged: {
Config.options.dock.revealDelayMs = value;
}
}
ConfigSpinBox {
icon: "hourglass_bottom"
text: Translation.tr("Hide delay (ms)")
value: Config.options.dock.hideDelayMs
from: 0
to: 2000
stepSize: 25
onValueChanged: {
Config.options.dock.hideDelayMs = value;
}
}
ConfigSpinBox {
icon: "timer"
text: Translation.tr("Drag dwell (ms)")
value: Config.options.dock.dragDwellMs
from: 100
to: 2000
stepSize: 50
onValueChanged: {
Config.options.dock.dragDwellMs = value;
}
StyledToolTip {
text: Translation.tr("How long a drag hovers an icon or stack before it reads as combine-intent.")
}
}
}
ContentSection {
icon: "palette"
title: Translation.tr("Appearance")
ConfigSwitch {
buttonIcon: "filter_b_and_w"
text: Translation.tr("Monochrome icons")
checked: Config.options.dock.monochromeIcons
onCheckedChanged: {
Config.options.dock.monochromeIcons = checked;
}
}
// The dock's own scale. Two numbers, because the button is the row's
// height and the icon is what you actually see — sizing one from the
// other would mean either cramped icons in a tall row or icons
// overflowing a short one, depending on which way the ratio was fixed.
ConfigSpinBox {
icon: "aspect_ratio"
text: Translation.tr("Button size (px)")
value: Config.options.dock.buttonSize
from: 36
to: 88
stepSize: 2
onValueChanged: {
Config.options.dock.buttonSize = value;
}
}
ConfigSpinBox {
icon: "apps"
text: Translation.tr("Icon size (px)")
value: Config.options.dock.iconSize
from: 24
to: 72
stepSize: 2
onValueChanged: {
Config.options.dock.iconSize = value;
}
}
ConfigSpinBox {
icon: "height"
text: Translation.tr("Dock height (px)")
value: Config.options.dock.height
from: 40
to: 120
stepSize: 2
onValueChanged: {
Config.options.dock.height = value;
}
}
ConfigSpinBox {
icon: "expand"
text: Translation.tr("Reveal region height (px)")
value: Config.options.dock.hoverRegionHeight
from: 1
to: 20
stepSize: 1
onValueChanged: {
Config.options.dock.hoverRegionHeight = value;
}
StyledToolTip {
text: Translation.tr("Height of the invisible bottom strip that triggers hover-reveal.")
}
}
}
ContentSection {
icon: "push_pin"
title: Translation.tr("Pinned apps")
StyledText {
Layout.fillWidth: true
text: Translation.tr("Drag on the dock is the primary way to manage these (drag onto an icon to stack, drag a member off the arc to split). This list is the fallback editor.")
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
wrapMode: Text.WordWrap
}
// One row per pinned app. Model binds the raw config list so writes
// (from here, the dock, or the agent) re-render immediately.
Repeater {
model: Config.options.dock.pinnedApps
delegate: RowLayout {
required property string modelData
Layout.fillWidth: true
spacing: 8
IconImage {
implicitSize: 24
source: Quickshell.iconPath(AppSearch.guessIcon(modelData), "image-missing")
}
StyledText {
Layout.fillWidth: true
text: modelData
elide: Text.ElideMiddle
color: Appearance.m3colors.m3onSurface
font.pixelSize: Appearance.font.pixelSize.small
}
RippleButton {
implicitWidth: 32
implicitHeight: 32
onClicked: TaskbarApps.togglePin(modelData)
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: "close"
iconSize: Appearance.font.pixelSize.normal
color: Appearance.m3colors.m3onSurface
}
StyledToolTip { text: Translation.tr("Unpin") }
}
}
}
StyledText {
visible: (Config.options.dock.pinnedApps?.length ?? 0) === 0
text: Translation.tr("Nothing pinned. Long-press a running app on the dock to pin it.")
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
}
}
ContentSection {
icon: "stacks"
title: Translation.tr("Stacks")
// One block per stack: editable name, then member rows. All edits go
// through TaskbarApps so the rules (id never changes, empty stacks
// dissolve, unstacked members re-pin) live in one place.
Repeater {
model: Config.options.dock.stacks
delegate: ColumnLayout {
id: stackBlock
required property string modelData
readonly property var stack: TaskbarApps.parseStack(modelData)
Layout.fillWidth: true
spacing: 2
RowLayout {
Layout.fillWidth: true
spacing: 8
MaterialSymbol {
text: "stacks"
iconSize: Appearance.font.pixelSize.large
color: Appearance.m3colors.m3onSurface
}
MaterialTextField {
Layout.fillWidth: true
text: stackBlock.stack.name
placeholderText: Translation.tr("Stack name")
onEditingFinished: {
if (text.length > 0 && text !== stackBlock.stack.name)
TaskbarApps.renameStack(stackBlock.stack.id, text);
}
}
RippleButton {
implicitWidth: 32
implicitHeight: 32
onClicked: {
// Dissolve: every member goes back to a pin.
for (const m of stackBlock.stack.members.slice())
TaskbarApps.unstackMember(stackBlock.stack.id, m);
}
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: "delete"
iconSize: Appearance.font.pixelSize.normal
color: Appearance.m3colors.m3onSurface
}
StyledToolTip { text: Translation.tr("Dissolve stack (members become pins)") }
}
}
Repeater {
model: stackBlock.stack.members
delegate: RowLayout {
required property string modelData
Layout.fillWidth: true
Layout.leftMargin: 28
spacing: 8
IconImage {
implicitSize: 22
source: Quickshell.iconPath(AppSearch.guessIcon(modelData), "image-missing")
}
StyledText {
Layout.fillWidth: true
text: modelData
elide: Text.ElideMiddle
color: Appearance.m3colors.m3onSurface
font.pixelSize: Appearance.font.pixelSize.small
}
RippleButton {
implicitWidth: 32
implicitHeight: 32
onClicked: TaskbarApps.unstackMember(stackBlock.stack.id, modelData)
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: "remove"
iconSize: Appearance.font.pixelSize.normal
color: Appearance.m3colors.m3onSurface
}
StyledToolTip { text: Translation.tr("Unstack (back to a pin)") }
}
}
}
}
}
StyledText {
visible: (Config.options.dock.stacks?.length ?? 0) === 0
text: Translation.tr("No stacks yet. Drag one dock icon onto another and hold until it highlights.")
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
}
}
}

View file

@ -0,0 +1,346 @@
import QtQuick
import QtQuick.Layouts
import qs.services
import qs.modules.common
import qs.modules.common.widgets
// Idle & sleep — the staged idle projection, exposed honestly.
//
// The governing idea is REFERENCE-EXTRACTION.md's "idle is a transition graph,
// not a timer": the page shows the live stage the shell is actually in, not
// just three timeout knobs pretending idle is linear.
//
// Two honesty constraints drive the layout, and both come straight from the
// extraction's build order (truth before visuals):
//
// 1. Two authorities, two sections, never blended. The shell's own
// IdleMonitors decide when a session IN USE dims and locks; sessiond's
// device state machine decides how long a LOCKED panel may burn. The
// second set is read live from the daemon (SessiondPolicy) rather than
// from a config file, because the daemon is what actuates them —
// TASK-19's "no success-shaped switches".
//
// hypridle no longer owns screen-off: its idle listeners were deleted
// 2026-07-25 once sessiond got real actuators, because "lock, then off"
// held only while its 300s lock happened to precede its 600s blank.
//
// 2. Settings is a window in the authoritative shell process, so this page
// reads the idle/session singletons directly. It must not spawn `qs ipc`
// children, which can become accidental shell instances when display
// selection is ambiguous.
ContentPage {
id: page
forceWidth: true
// --- Live stage readout ----------------------------------------------
// 0 Active · 1 Dimmed · 2 Lock requested · 3 Lock secure ·
// 4 Suspending · 5 Asleep · 6 Waking — the IdleCoordinator.State
// enum, read directly from the shell-owned coordinator.
readonly property int liveStage: IdleCoordinator.state
readonly property bool liveNative: IdleCoordinator.nativeEnabled
readonly property bool probeOk: true
readonly property bool sleepInhibitorHeld: SessionEvents.sleepInhibitorHeld
readonly property bool stepUpEnabled: StepUpAuth.grantTtlMs > 0
// Seconds on the wire, human words on screen. "Never" is 0, which is what
// the daemon already means by it.
readonly property var blankPresets: [
{ displayName: Translation.tr("15s"), icon: "timer", value: 15 },
{ displayName: Translation.tr("30s"), icon: "timer", value: 30 },
{ displayName: Translation.tr("1 min"), icon: "timer", value: 60 },
{ displayName: Translation.tr("5 min"), icon: "timer", value: 300 },
{ displayName: Translation.tr("Never"), icon: "timer_off", value: 0 }
]
readonly property var idlePresets: [
{ displayName: Translation.tr("30s"), icon: "timer", value: 30 },
{ displayName: Translation.tr("1 min"), icon: "timer", value: 60 },
{ displayName: Translation.tr("2 min"), icon: "timer", value: 120 },
{ displayName: Translation.tr("5 min"), icon: "timer", value: 300 },
{ displayName: Translation.tr("10 min"), icon: "timer", value: 600 }
]
// ONE dim setting, for both authorities.
//
// This used to be two: a session dim (15s–5min, before the lock) and a
// separate lock-screen dim grace (5–20s, before the blank). They are two
// daemons, but they are not two questions — the user is answering "how
// much warning do I get before the screen goes away", once. Splitting it
// made the page describe our architecture instead of their screen.
//
// Written to both, unchanged: the shell's dimBeforeLockSeconds and
// sessiond's dim_grace_secs.
readonly property var dimPresets: [
{ displayName: Translation.tr("5s"), icon: "brightness_medium", value: 5 },
{ displayName: Translation.tr("10s"), icon: "brightness_medium", value: 10 },
{ displayName: Translation.tr("30s"), icon: "brightness_medium", value: 30 },
{ displayName: Translation.tr("1 min"), icon: "brightness_medium", value: 60 },
{ displayName: Translation.tr("3 min"), icon: "brightness_medium", value: 180 }
]
// Write the chosen value through, unchanged, to both authorities.
//
// An earlier version clamped the lock-screen grace to `blank - 1` so a
// long dim would still "fit". That was invented policy nobody asked for
// and it inverted the setting: a 30 s dim against a 15 s blank became a
// 14 s grace, which starts the dim one second after you stop touching the
// phone. A setting that silently means something else is worse than one
// that does not apply.
function applyDim(v) {
Config.options.lock.idle.dimBeforeLockSeconds = v;
if (SessiondPolicy.available)
SessiondPolicy.apply({ dim_grace_secs: v });
}
// "2 min" reads better than "120 s" in a sentence about when things happen.
function clockText(secs) {
if (secs >= 60 && secs % 60 === 0)
return Translation.tr("%1 min").arg(secs / 60);
if (secs >= 60)
return Translation.tr("%1 min %2 s").arg(Math.floor(secs / 60)).arg(secs % 60);
return Translation.tr("%1 s").arg(secs);
}
readonly property var stageNames: [
Translation.tr("Active"),
Translation.tr("Dimmed"),
Translation.tr("Lock requested"),
Translation.tr("Lock secure"),
Translation.tr("Suspending"),
Translation.tr("Asleep"),
Translation.tr("Waking")
]
function stageLabel(s) {
return (s >= 0 && s < stageNames.length) ? stageNames[s] : Translation.tr("unknown");
}
ContentSection {
icon: "motion_sensor_active"
title: Translation.tr("Current state")
RowLayout {
Layout.fillWidth: true
spacing: 12
StyledText {
text: page.probeOk
? Translation.tr("Idle stage: %1").arg(page.stageLabel(page.liveStage))
: Translation.tr("Idle stage: (shell not reachable)")
font.pixelSize: Appearance.font.pixelSize.normal
}
}
StyledText {
Layout.fillWidth: true
wrapMode: Text.WordWrap
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
text: page.liveNative
? Translation.tr("The native coordinator is driving idle transitions. Dim and lock fire on the timers below.")
: Translation.tr("The native coordinator is off, so the session timers below do not run. Nothing else locks on idle now that hypridle's listeners are gone — turn it on, or the phone only locks when you lock it.")
}
RowLayout {
Layout.fillWidth: true
spacing: 12
StyledText {
text: page.sleepInhibitorHeld
? Translation.tr("Sleep inhibitor: held (suspend blocked until lock is secure)")
: Translation.tr("Sleep inhibitor: released (suspend may proceed)")
font.pixelSize: Appearance.font.pixelSize.normal
color: page.sleepInhibitorHeld
? Appearance.colors.colOnLayer1
: Appearance.colors.colSubtext
}
}
}
ContentSection {
icon: "experiment"
title: Translation.tr("Native idle coordinator")
ConfigSwitch {
buttonIcon: "science"
text: Translation.tr("Enable native coordinator (experimental)")
checked: Config.options.lock.idle.nativeCoordinatorEnabled
onCheckedChanged: {
Config.options.lock.idle.nativeCoordinatorEnabled = checked;
}
StyledToolTip {
text: Translation.tr("Use Wayland idle-notify to drive the dim/lock timers. Unverified on the Pixel 3 compositor build — leave off unless you are testing it. When off, hypridle handles idle and screen-off.")
}
}
}
ContentSection {
icon: "timer"
title: Translation.tr("Session timers")
// Config keys, effective only while the native coordinator is on.
// Presets rather than a seconds spinner, same reasoning as below.
ContentSubsectionLabel {
text: Translation.tr("Lock the session after")
}
ConfigSelectionArray {
currentValue: Config.options.lock.idle.lockAfterSeconds
onSelected: v => Config.options.lock.idle.lockAfterSeconds = v
options: page.idlePresets
}
StyledText {
Layout.fillWidth: true
visible: !Config.options.lock.idle.nativeCoordinatorEnabled
wrapMode: Text.WordWrap
color: Appearance.colors.colError
font.pixelSize: Appearance.font.pixelSize.smaller
text: Translation.tr("The native coordinator is off, so these do not run.")
}
}
// --- Brightness dimming, one question ---------------------------------
// Deliberately not split by authority. Two daemons own the actuation, and
// the page used to say so by giving each its own dim control — which made
// the user answer the same question twice and left them to work out that
// the two interact. One control, written to both. See applyDim().
ContentSection {
icon: "brightness_medium"
title: Translation.tr("Brightness dimming")
ConfigSwitch {
buttonIcon: "brightness_low"
text: Translation.tr("Dim before the screen goes away")
checked: SessiondPolicy.dimWarning
enabled: SessiondPolicy.available
onCheckedChanged: {
if (SessiondPolicy.available && checked !== SessiondPolicy.dimWarning)
SessiondPolicy.apply({ dim_warning: checked });
}
}
ContentSubsectionLabel {
text: Translation.tr("Start dimming this long before")
}
ConfigSelectionArray {
currentValue: Config.options.lock.idle.dimBeforeLockSeconds
onSelected: v => page.applyDim(v)
options: page.dimPresets
}
// Say what it actually does, on both surfaces, in one sentence each.
// A relationship the user has to compute in their head is one they
// will get wrong.
StyledText {
Layout.fillWidth: true
wrapMode: Text.WordWrap
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
text: {
const lock = Config.options.lock.idle.lockAfterSeconds;
const grace = Config.options.lock.idle.dimBeforeLockSeconds;
const dimAt = Math.max(1, Math.min(lock - 1, lock - grace));
return Translation.tr("In use: dims at %1, locks at %2.")
.arg(page.clockText(dimAt))
.arg(page.clockText(lock));
}
}
StyledText {
Layout.fillWidth: true
visible: SessiondPolicy.available
wrapMode: Text.WordWrap
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
text: {
const blank = SessiondPolicy.lockBlankAfterSecs;
if (blank === 0)
return Translation.tr("On the lock screen: never blanks, so nothing dims.");
const g = SessiondPolicy.dimGraceSecs;
return Translation.tr("On the lock screen: dims %1 before blanking at %2.")
.arg(page.clockText(g))
.arg(page.clockText(blank));
}
}
}
// --- Lock screen timers, owned by sessiond ----------------------------
// A separate section because these are a different authority. The two
// above are the shell's own IdleMonitors deciding when to dim and lock a
// session in use; these are the device state machine deciding how long a
// LOCKED, lit panel may burn before it goes dark. That machine has its own
// clock and its own actuators, so its numbers must come from it — which is
// what SessiondPolicy is for, and what this page did not do until
// 2026-07-25 (SetPolicy had zero callers; the daemon ran on its built-in
// 15 s while this page showed whatever was in the JSON file).
Component.onCompleted: SessiondPolicy.refresh()
ContentSection {
icon: "phonelink_lock"
title: Translation.tr("Lock screen (device authority)")
StyledText {
Layout.fillWidth: true
wrapMode: Text.WordWrap
color: SessiondPolicy.available
? Appearance.colors.colSubtext
: Appearance.colors.colError
font.pixelSize: Appearance.font.pixelSize.smaller
text: SessiondPolicy.available
? Translation.tr("Read live from souveraine-sessiond. Changes here go straight to the daemon that blanks the panel.")
: Translation.tr("sessiond is not answering — these values are NOT authoritative. %1").arg(SessiondPolicy.lastError)
}
// Presets, not a seconds spinner. The policy struct's own comment
// names the shape — "iOS's Auto-Lock: a user-chosen timeout with a
// visible dim shortly before it, and a 'never' option for the
// desk-clock case" — and nobody reasons about a lock screen in
// 5-second increments. Values are still seconds on the wire; the
// daemon's vocabulary does not change because the UI got legible.
// One blank timeout, not two.
//
// The daemon still has a separate held-vs-resting budget, and it is
// still the right idea — a phone in your hand should not blank on the
// same schedule as one face-up on a desk. It is not a *setting*
// though. Asking the user to pick two numbers made them responsible
// for arbitrating a guess the accelerometer was making on their
// behalf, and the machine already has a better vocabulary for that:
// §4's confidence arithmetic. Held-ness belongs there, as an
// adjustment to one budget, not as a second budget on this page.
//
// Until that lands, both fields get the same value, so the behaviour
// is uniform and predictable rather than silently forking on a sensor
// reading nothing surfaces.
ContentSubsectionLabel {
text: Translation.tr("Blank after")
}
ConfigSelectionArray {
currentValue: SessiondPolicy.lockBlankAfterSecs
onSelected: v => SessiondPolicy.apply({
lock_blank_after_secs: v,
lock_blank_after_held_secs: v
})
options: page.blankPresets
}
// Only claim the ordering guarantee when the daemon actually reports
// the field. An older sessiond has no lock_ack_budget_secs, and
// rendering that absence as "waits 0s" would be the page inventing a
// number — the same class of lie as the timers this section replaced.
StyledText {
Layout.fillWidth: true
visible: SessiondPolicy.available && SessiondPolicy.lockAckBudgetSecs > 0
wrapMode: Text.WordWrap
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
text: Translation.tr("The screen never goes dark on an unlocked session. sessiond locks first and waits %1s for the compositor to acknowledge; if it cannot, it blanks anyway and records a security error rather than pretending the session locked.")
.arg(SessiondPolicy.lockAckBudgetSecs)
}
StyledText {
Layout.fillWidth: true
visible: SessiondPolicy.available && SessiondPolicy.lockAckBudgetSecs === 0
wrapMode: Text.WordWrap
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
text: Translation.tr("This sessiond predates the lock-before-blank ordering. Update the souveraine package to get it.")
}
}
}

View file

@ -0,0 +1,32 @@
import QtQuick
import QtQuick.Layouts
import qs.services
import qs.modules.common
import qs.modules.common.widgets
// On-screen keyboard.
ContentPage {
forceWidth: true
ContentSection {
icon: "keyboard"
title: Translation.tr("On-screen keyboard")
ConfigSwitch {
buttonIcon: "push_pin"
text: Translation.tr("Pinned on startup")
checked: Config.options.osk.pinnedOnStartup
onCheckedChanged: {
Config.options.osk.pinnedOnStartup = checked;
}
StyledToolTip {
text: Translation.tr("Keep the on-screen keyboard visible from launch rather than on demand.")
}
}
// osk.layout is intentionally not exposed here: its value space
// (adapter default "qwerty_full" vs the layouts.js byName registry
// keyed by display name like "English (US)") is inconsistent and
// needs reconciling before a selector is honest about it.
}
}

View file

@ -0,0 +1,320 @@
import QtQuick
import QtQuick.Layouts
import Qt.labs.folderlistmodel
import Quickshell
import qs.services
import qs.modules.common
import qs.modules.common.widgets
// Lock screen — behavior + appearance. Pure Config bindings: every control
// writes an option that already exists in Config.qml's adapter, so
// persistence + hot-apply come for free.
ContentPage {
forceWidth: true
ContentSection {
icon: "lock"
title: Translation.tr("Behavior")
ConfigSwitch {
buttonIcon: "pin"
text: Translation.tr("Touch keypad (phone)")
checked: Config.options.lock.touchKeypad
onCheckedChanged: {
Config.options.lock.touchKeypad = checked;
}
StyledToolTip {
text: Translation.tr("Show the on-lock PIN keypad. The on-screen keyboard can't rise above a session lock, so the lock surface carries its own input.")
}
}
ConfigSwitch {
buttonIcon: "rocket_launch"
text: Translation.tr("Launch lock on startup")
checked: Config.options.lock.launchOnStartup
onCheckedChanged: {
Config.options.lock.launchOnStartup = checked;
}
StyledToolTip {
text: Translation.tr("Start the session locked so a PIN is required before the shell is exposed.")
}
}
ConfigSwitch {
buttonIcon: "key_off"
text: Translation.tr("Require password to power off")
checked: Config.options.lock.security.requirePasswordToPower
onCheckedChanged: {
Config.options.lock.security.requirePasswordToPower = checked;
}
StyledToolTip {
text: Translation.tr("Guard the power menu behind the lock so the device can't be silenced without a PIN.")
}
}
ConfigSwitch {
buttonIcon: "power_settings_new"
text: Translation.tr("Allow power off / reboot from lock screen")
checked: Config.options.lock.security.allowPowerFromLock
onCheckedChanged: {
Config.options.lock.security.allowPowerFromLock = checked;
}
StyledToolTip {
text: Translation.tr("Show the power and reboot buttons on the lock screen. Off by default — a destructive action from the locked surface is opt-in. The buttons still respect “require password to power off” above.")
}
}
ConfigSwitch {
buttonIcon: "vpn_key"
text: Translation.tr("Unlock keyring on PIN unlock")
checked: Config.options.lock.security.unlockKeyring
onCheckedChanged: {
Config.options.lock.security.unlockKeyring = checked;
}
StyledToolTip {
text: Translation.tr("Feed the PIN to the keyring so stored secrets unlock together with the session.")
}
}
}
ContentSection {
icon: "palette"
title: Translation.tr("Appearance")
ConfigSwitch {
buttonIcon: "format_align_center"
text: Translation.tr("Center the clock")
checked: Config.options.lock.centerClock
onCheckedChanged: {
Config.options.lock.centerClock = checked;
}
}
ConfigSwitch {
buttonIcon: "schedule"
text: Translation.tr("12-hour clock (am/pm)")
checked: Config.options.lock.twelveHourClock
onCheckedChanged: {
Config.options.lock.twelveHourClock = checked;
}
}
ConfigSwitch {
buttonIcon: "text_fields"
text: Translation.tr("Show locked text")
checked: Config.options.lock.showLockedText
onCheckedChanged: {
Config.options.lock.showLockedText = checked;
}
}
ConfigSwitch {
buttonIcon: "blur_on"
text: Translation.tr("Blur background")
checked: Config.options.lock.blur.enable
onCheckedChanged: {
Config.options.lock.blur.enable = checked;
}
}
ConfigSwitch {
buttonIcon: "category"
text: Translation.tr("Material shapes for PIN dots")
checked: Config.options.lock.materialShapeChars
onCheckedChanged: {
Config.options.lock.materialShapeChars = checked;
}
}
}
ContentSection {
id: lockWallSection
icon: "wallpaper"
title: Translation.tr("Lock screen wallpaper")
readonly property string wallpaperDir: Quickshell.env("HOME") + "/Pictures/Wallpapers"
ConfigSwitch {
buttonIcon: "sync"
text: Translation.tr("Follow system wallpaper")
checked: !Config.options.lock.wallpaperPath
onCheckedChanged: {
if (checked) Config.options.lock.wallpaperPath = "";
}
StyledToolTip {
text: Translation.tr("The lock screen shows the same wallpaper as the shell. Pick an image below to pin the lock screen's own.")
}
}
GridView {
id: lockWallGrid
Layout.fillWidth: true
readonly property int cols: 3
cellWidth: Math.floor(width / cols)
cellHeight: Math.floor(cellWidth * 2)
implicitHeight: Math.ceil(lockWallModel.count / cols) * cellHeight
interactive: false
clip: true
model: FolderListModel {
id: lockWallModel
folder: "file://" + lockWallSection.wallpaperDir
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.webp"]
showDirs: false
}
delegate: Item {
required property string filePath
width: lockWallGrid.cellWidth
height: lockWallGrid.cellHeight
Rectangle {
anchors.fill: parent
anchors.margins: 5
radius: Appearance.rounding.small
color: Appearance.colors.colLayer1
border.width: Config.options.lock.wallpaperPath === filePath ? 3 : 0
border.color: Appearance.colors.colPrimary
clip: true
Image {
anchors.fill: parent
anchors.margins: 3
source: "file://" + filePath
fillMode: Image.PreserveAspectCrop
asynchronous: true
sourceSize.width: 240
}
MouseArea {
anchors.fill: parent
onClicked: Config.options.lock.wallpaperPath = filePath
}
}
}
}
}
ContentSection {
icon: "visibility"
title: Translation.tr("Lock screen content")
ConfigSwitch {
buttonIcon: "music_note"
text: Translation.tr("Show media controls")
checked: Config.options.lock.content.showMediaControls
onCheckedChanged: Config.options.lock.content.showMediaControls = checked
StyledToolTip {
text: Translation.tr("Shows previous, play/pause, and next. Track details remain private unless enabled below.")
}
}
ConfigSwitch {
buttonIcon: "visibility"
text: Translation.tr("Show media title and artist")
checked: Config.options.lock.content.mediaMetadataAmbient
enabled: Config.options.lock.content.showMediaControls
onCheckedChanged: Config.options.lock.content.mediaMetadataAmbient = checked
StyledToolTip {
text: Translation.tr("Treats current media metadata as ambient. Leave off to keep it hidden until unlock.")
}
}
ConfigSwitch {
buttonIcon: "battery_android_full"
text: Translation.tr("Show battery on lock screen")
checked: Config.options.lock.content.showBattery
onCheckedChanged: Config.options.lock.content.showBattery = checked
}
ConfigSwitch {
buttonIcon: "notifications"
text: Translation.tr("Show notifications while locked")
checked: Config.options.lock.content.showNotifications
onCheckedChanged: Config.options.lock.content.showNotifications = checked
StyledToolTip {
text: Translation.tr("Shows which apps had notifications arrive and how many. Content stays private unless enabled below.")
}
}
ConfigSwitch {
buttonIcon: "visibility"
text: Translation.tr("Show notification summaries")
checked: Config.options.lock.content.notificationContentAmbient
enabled: Config.options.lock.content.showNotifications
onCheckedChanged: Config.options.lock.content.notificationContentAmbient = checked
StyledToolTip {
text: Translation.tr("Treats the notification title line as ambient. Bodies never show on the lock screen.")
}
}
}
ContentSection {
icon: "key"
title: Translation.tr("Step-up authentication")
ConfigSwitch {
buttonIcon: "shield_lock"
text: Translation.tr("Enable step-up authentication")
checked: Config.options.lock.stepUp.enabled
onCheckedChanged: Config.options.lock.stepUp.enabled = checked
StyledToolTip {
text: Translation.tr("Require re-authentication for sensitive operations (send, delete, payment). Requires the souveraine-stepup PAM service to be installed on the system.")
}
}
ConfigSpinBox {
icon: "timer"
text: Translation.tr("Grant validity (seconds)")
value: Config.options.lock.stepUp.grantTtlMs / 1000
from: 30
to: 3600
stepSize: 30
enabled: Config.options.lock.stepUp.enabled
onValueChanged: Config.options.lock.stepUp.grantTtlMs = value * 1000
StyledToolTip {
text: Translation.tr("How long a step-up grant remains valid after authentication. The user can perform sensitive operations within this window without re-authenticating.")
}
}
}
ContentSection {
icon: "fingerprint"
title: Translation.tr("Fingerprint integration")
ConfigSwitch {
buttonIcon: "fingerprint"
text: Translation.tr("Show fingerprint wiring preview")
checked: Config.options.lock.fingerprintPreview.enabled
onCheckedChanged: Config.options.lock.fingerprintPreview.enabled = checked
StyledToolTip {
text: Translation.tr("Shows a three-second hold test on the lock screen. It never unlocks the device; post-login Polkit is configured separately below.")
}
}
ConfigSpinBox {
icon: "timer"
text: Translation.tr("Preview hold (seconds)")
value: Config.options.lock.fingerprintPreview.holdMs / 1000
from: 1
to: 10
stepSize: 1
enabled: Config.options.lock.fingerprintPreview.enabled
onValueChanged: Config.options.lock.fingerprintPreview.holdMs = value * 1000
StyledToolTip {
text: Translation.tr("This controls the visible lock-screen exercise only. It does not change the temporary post-login Polkit factor.")
}
}
ConfigSwitch {
buttonIcon: "admin_panel_settings"
text: Translation.tr("Allow temporary FPC confirmation for Polkit")
checked: Config.options.lock.fingerprintPolkit.enabled
onCheckedChanged: Config.options.lock.fingerprintPolkit.enabled = checked
StyledToolTip {
text: Translation.tr("After PIN login, user-facing Polkit prompts may accept a fresh reader assertion after the visible confirmation interval. It never unlocks the session or replaces first-login PIN.")
}
}
}
}

View file

@ -0,0 +1,42 @@
import QtQuick
import QtQuick.Layouts
import qs.services
import qs.modules.common
import qs.modules.common.widgets
// Souveraine's integrated phone navigation surface.
ContentPage {
forceWidth: true
ContentSection {
icon: "gesture"
title: Translation.tr("Layout")
ConfigSpinBox {
icon: "swap_vert"
text: Translation.tr("Navigation rail height (px)")
value: Config.options.dock.gestureRailHeight
from: 0
to: 96
stepSize: 2
onValueChanged: Config.options.dock.gestureRailHeight = value
StyledToolTip {
text: Translation.tr("Bottom strip reserved for Souveraine navigation — visually and in the dock's input mask. The navigation rail must always win touch here.")
}
}
}
ContentSection {
icon: "swipe"
title: Translation.tr("Gestures")
StyledText {
Layout.fillWidth: true
text: Translation.tr("Double-tap toggles app fullscreen. Swipe up reveals the dock. Swipe down dismisses the nearest surface: the keyboard when it's open (the rail rides on top of it), otherwise a visible dock — pinned included. Timing and threshold controls will appear here once their defaults prove out.")
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
wrapMode: Text.WordWrap
}
}
}

View file

@ -0,0 +1,199 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Bluetooth
import qs.services
import qs.modules.common
import qs.modules.common.widgets
// First-party connectivity page. The controls are direct views over the
// resident Network/Cellular/Bluetooth services; no settings-only state is
// allowed to pretend a radio changed when its owning service did not.
ContentPage {
id: page
forceWidth: true
function wifiIcon(strength) {
return strength > 80 ? "signal_wifi_4_bar"
: strength > 60 ? "network_wifi_3_bar"
: strength > 40 ? "network_wifi_2_bar"
: strength > 20 ? "network_wifi_1_bar"
: "signal_wifi_0_bar";
}
ContentSection {
icon: "wifi"
title: Translation.tr("Wi-Fi")
ConfigSwitch {
buttonIcon: Network.materialSymbol
text: Network.wifiEnabled
? Translation.tr("Wi-Fi on") : Translation.tr("Wi-Fi off")
checked: Network.wifiEnabled
onCheckedChanged: {
if (checked !== Network.wifiEnabled) Network.enableWifi(checked);
}
}
RowLayout {
Layout.fillWidth: true
spacing: 10
StyledText {
Layout.fillWidth: true
text: Network.active
? Translation.tr("Connected to %1").arg(Network.active.ssid)
: Translation.tr("Not connected")
color: Appearance.colors.colSubtext
elide: Text.ElideRight
}
RippleButton {
implicitWidth: 44
implicitHeight: 44
enabled: Network.wifiEnabled && !Network.wifiScanning
buttonRadius: Appearance.rounding.full
onClicked: Network.rescanWifi()
contentItem: MaterialSymbol {
anchors.centerIn: parent
text: Network.wifiScanning ? "progress_activity" : "refresh"
iconSize: 21
}
StyledToolTip { text: Translation.tr("Scan for networks") }
}
}
Repeater {
model: Network.wifiEnabled ? Network.friendlyWifiNetworks : []
delegate: DialogListItem {
id: networkRow
required property var modelData
Layout.fillWidth: true
active: modelData.active
buttonRadius: Appearance.rounding.normal
onClicked: {
if (modelData.active) Network.disconnectWifiNetwork();
else Network.connectToWifiNetwork(modelData);
}
contentItem: ColumnLayout {
anchors {
fill: parent
leftMargin: networkRow.horizontalPadding
rightMargin: networkRow.horizontalPadding
topMargin: networkRow.verticalPadding
bottomMargin: networkRow.verticalPadding
}
spacing: 8
RowLayout {
Layout.fillWidth: true
spacing: 10
MaterialSymbol {
text: page.wifiIcon(networkRow.modelData.strength)
iconSize: 22
}
StyledText {
Layout.fillWidth: true
text: networkRow.modelData.ssid
textFormat: Text.PlainText
elide: Text.ElideRight
}
MaterialSymbol {
text: networkRow.modelData.active ? "check"
: networkRow.modelData.isSecure ? "lock" : ""
iconSize: 20
}
}
MaterialTextField {
Layout.fillWidth: true
visible: networkRow.modelData.askingPassword
placeholderText: Translation.tr("Network password")
echoMode: TextInput.Password
inputMethodHints: Qt.ImhSensitiveData
onAccepted: {
Network.changePassword(networkRow.modelData, text);
text = "";
}
}
}
}
}
}
ContentSection {
icon: Cellular.materialSymbol
title: Translation.tr("Mobile network")
RowLayout {
Layout.fillWidth: true
spacing: 12
ColumnLayout {
Layout.fillWidth: true
spacing: 2
StyledText {
Layout.fillWidth: true
text: Cellular.available
? (Cellular.operatorName || Translation.tr("Mobile network"))
: Translation.tr("No modem detected")
elide: Text.ElideRight
}
StyledText {
Layout.fillWidth: true
text: Cellular.available
? Translation.tr("%1 · %2% signal%3")
.arg(Cellular.accessTech || Cellular.state)
.arg(Cellular.signalQuality)
.arg(Cellular.roaming ? Translation.tr(" · roaming") : "")
: Translation.tr("ModemManager has not exposed a modem")
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
wrapMode: Text.WordWrap
}
}
RippleButton {
implicitWidth: 44
implicitHeight: 44
buttonRadius: Appearance.rounding.full
onClicked: Cellular.update()
contentItem: MaterialSymbol {
anchors.centerIn: parent
text: "refresh"
iconSize: 21
}
}
}
}
ContentSection {
icon: "bluetooth"
title: Translation.tr("Bluetooth")
ConfigSwitch {
buttonIcon: BluetoothStatus.connected ? "bluetooth_connected" : "bluetooth"
text: BluetoothStatus.available
? Translation.tr("Bluetooth") : Translation.tr("Bluetooth unavailable")
enabled: BluetoothStatus.available
checked: BluetoothStatus.enabled
onCheckedChanged: {
if (Bluetooth.defaultAdapter && checked !== Bluetooth.defaultAdapter.enabled)
Bluetooth.defaultAdapter.enabled = checked;
}
}
StyledText {
Layout.fillWidth: true
text: !BluetoothStatus.available
? Translation.tr("BlueZ has not exposed an adapter; no success-shaped toggle is shown.")
: BluetoothStatus.activeDeviceCount > 0
? Translation.tr("%1 connected device(s)").arg(BluetoothStatus.activeDeviceCount)
: Translation.tr("No connected devices")
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
wrapMode: Text.WordWrap
}
}
}

View file

@ -0,0 +1,80 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import qs.services
import qs.modules.common
import qs.modules.common.widgets
// Home screen (app drawer) — the TASK-14 split's launcher half. The drawer
// reads its grid straight from Config (see AppGrid.qml), so these knobs are
// the single source; the grid re-lays-out on the next drawer open. Rows and
// columns at the phone's 540x1080: 4 columns keeps a thumb travel across a
// page, 5 rows fills the panel without scrolling. The old desktop overview's
// rows/columns are untouched — this page owns Config.options.overview.appGrid.
ContentPage {
forceWidth: true
ContentSection {
icon: "grid_view"
title: Translation.tr("App drawer")
ConfigSwitch {
buttonIcon: "apps"
text: Translation.tr("Enable drawer")
checked: Config.options.overview.enable
onCheckedChanged: {
Config.options.overview.enable = checked;
}
StyledToolTip {
text: Translation.tr("The Home surface: search on top, the app grid below. Off, the pill's swipe-home does nothing.")
}
}
ConfigSpinBox {
icon: "view_column"
text: Translation.tr("Columns")
value: Config.options.overview.appGrid.columns
from: 1
to: 10
stepSize: 1
onValueChanged: {
Config.options.overview.appGrid.columns = value;
}
StyledToolTip {
text: Translation.tr("Icons per row. More columns = denser grid and smaller tiles.")
}
}
ConfigSpinBox {
icon: "view_agenda"
text: Translation.tr("Rows")
value: Config.options.overview.appGrid.rows
from: 1
to: 10
stepSize: 1
onValueChanged: {
Config.options.overview.appGrid.rows = value;
}
StyledToolTip {
text: Translation.tr("Icon rows per page. More rows fills the screen; fewer leaves room for the page dots.")
}
}
ConfigSpinBox {
icon: "photo_size_select_large"
text: Translation.tr("Icon size (px)")
value: Config.options.overview.appGrid.iconSize
from: 24
to: 96
stepSize: 4
onValueChanged: {
Config.options.overview.appGrid.iconSize = value;
}
StyledToolTip {
text: Translation.tr("The icon glyph itself; labels always scale to the tile.")
}
}
}
}

View file

@ -0,0 +1,93 @@
import QtQuick
import QtQuick.Layouts
import qs.services
import qs.modules.common
import qs.modules.common.widgets
ContentPage {
forceWidth: true
ContentSection {
icon: "volume_up"
title: Translation.tr("Output")
StyledText {
Layout.fillWidth: true
text: Audio.sink ? Audio.friendlyDeviceName(Audio.sink)
: Translation.tr("No audio output")
color: Appearance.colors.colSubtext
elide: Text.ElideRight
}
RowLayout {
Layout.fillWidth: true
spacing: 12
RippleButton {
implicitWidth: 44
implicitHeight: 44
enabled: Audio.sink ? Audio.sink.ready : false
buttonRadius: Appearance.rounding.full
onClicked: Audio.toggleMute()
contentItem: MaterialSymbol {
anchors.centerIn: parent
text: Audio.sink && Audio.sink.audio.muted ? "volume_off" : "volume_up"
iconSize: 22
}
}
StyledSlider {
Layout.fillWidth: true
enabled: Audio.sink ? Audio.sink.ready : false
value: Audio.sink ? Audio.sink.audio.volume : 0
from: 0
to: 1
onMoved: {
if (Audio.sink) Audio.sink.audio.volume = value;
}
}
}
}
ContentSection {
icon: "mic"
title: Translation.tr("Microphone")
StyledText {
Layout.fillWidth: true
text: Audio.source ? Audio.friendlyDeviceName(Audio.source)
: Translation.tr("No microphone")
color: Appearance.colors.colSubtext
elide: Text.ElideRight
}
RowLayout {
Layout.fillWidth: true
spacing: 12
RippleButton {
implicitWidth: 44
implicitHeight: 44
enabled: Audio.source ? Audio.source.ready : false
buttonRadius: Appearance.rounding.full
onClicked: Audio.toggleMicMute()
contentItem: MaterialSymbol {
anchors.centerIn: parent
text: Audio.source && Audio.source.audio.muted ? "mic_off" : "mic"
iconSize: 22
}
StyledToolTip {
text: Audio.source && Audio.source.audio.muted
? Translation.tr("Unmute microphone") : Translation.tr("Mute microphone")
}
}
StyledSlider {
Layout.fillWidth: true
enabled: Audio.source ? Audio.source.ready : false
value: Audio.source ? Audio.source.audio.volume : 0
from: 0
to: 1
onMoved: {
if (Audio.source) Audio.source.audio.volume = value;
}
}
}
}
}

View file

@ -0,0 +1,135 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Io
import qs.services
import qs.modules.common
import qs.modules.common.widgets
// Speech services — dictation (STT) and speech synthesis (TTS).
// The endpoints live in Config so the souveraine-stt CLI (and the
// keyboard's mic key that shells out to it) read exactly what's set
// here. The probe hits the STT /health route so "it's configured" and
// "it's answering" are visibly different states.
ContentPage {
forceWidth: true
ContentSection {
icon: "mic"
title: Translation.tr("Dictation (speech to text)")
ConfigSwitch {
buttonIcon: "record_voice_over"
text: Translation.tr("Enable dictation")
checked: Config.options.speech.stt.enable
onCheckedChanged: {
Config.options.speech.stt.enable = checked;
}
StyledToolTip {
text: Translation.tr("Voice input through the transcription server. The keyboard's mic key and the souveraine-stt command both use this.")
}
}
MaterialTextField {
Layout.fillWidth: true
text: Config.options.speech.stt.endpoint
placeholderText: Translation.tr("Transcription endpoint (http://host:port/transcribe)")
onEditingFinished: {
if (text !== Config.options.speech.stt.endpoint) {
Config.options.speech.stt.endpoint = text;
healthProbe.refresh();
}
}
}
RowLayout {
spacing: 8
StyledText {
text: healthProbe.statusText
color: healthProbe.healthy ? Appearance.colors.colOnLayer1
: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
}
RippleButton {
implicitWidth: 32
implicitHeight: 32
buttonRadius: Appearance.rounding.full
onClicked: healthProbe.refresh()
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: "refresh"
iconSize: 18
}
StyledToolTip {
text: Translation.tr("Check the server")
}
}
}
}
ContentSection {
icon: "text_to_speech"
title: Translation.tr("Speech synthesis (text to speech)")
ConfigSwitch {
buttonIcon: "campaign"
text: Translation.tr("Enable speech output")
checked: Config.options.speech.tts.enable
onCheckedChanged: {
Config.options.speech.tts.enable = checked;
}
StyledToolTip {
text: Translation.tr("Spoken responses through the synthesis server. Off until a TTS endpoint exists.")
}
}
MaterialTextField {
Layout.fillWidth: true
text: Config.options.speech.tts.endpoint
placeholderText: Translation.tr("Synthesis endpoint (empty = none yet)")
onEditingFinished: {
if (text !== Config.options.speech.tts.endpoint)
Config.options.speech.tts.endpoint = text;
}
}
}
// STT /health probe. Derives the health URL from the transcribe
// endpoint (…/transcribe -> …/health) rather than storing a second URL.
QtObject {
id: healthProbe
property bool healthy: false
property string statusText: Translation.tr("Checking server…")
function refresh() {
statusText = Translation.tr("Checking server…");
healthy = false;
probeProc.running = false;
probeProc.running = true;
}
}
Process {
id: probeProc
running: true
command: ["curl", "-s", "--max-time", "5",
Config.options.speech.stt.endpoint.replace(/\/[^\/]*$/, "/health")]
stdout: StdioCollector {
onStreamFinished: {
try {
const h = JSON.parse(text);
healthProbe.healthy = h.status === "ok";
healthProbe.statusText = healthProbe.healthy
? Translation.tr("Server up · %1 on %2").arg(h.model ?? "?").arg(h.device ?? "?")
: Translation.tr("Server answered but not ok");
} catch (e) {
healthProbe.healthy = false;
healthProbe.statusText = Translation.tr("Server unreachable");
}
}
}
}
}

View file

@ -0,0 +1,278 @@
import QtQuick
import QtQuick.Layouts
import Qt.labs.folderlistmodel
import Quickshell
import qs.services
import qs.modules.common
import qs.modules.common.widgets
// Wallpaper — pick from ~/Pictures/Wallpapers inside the settings app.
// Settings is a window in the main shell process, so applying calls the
// shell-owned Wallpapers singleton directly. Never spawn a nested `qs ipc`
// process from this page.
ContentPage {
forceWidth: true
readonly property string homeDir: Quickshell.env("HOME")
property string currentDir: homeDir + "/Pictures/Wallpapers"
property var quickDirs: [
{ name: "Wallpapers", path: homeDir + "/Pictures/Wallpapers", icon: "wallpaper" },
{ name: "Pictures", path: homeDir + "/Pictures", icon: "image" },
{ name: "Downloads", path: homeDir + "/Downloads", icon: "download" },
{ name: "Home", path: homeDir, icon: "home" }
]
ContentSection {
icon: "wallpaper"
title: Translation.tr("Wallpaper")
StyledText {
Layout.fillWidth: true
text: Translation.tr("Current: %1").arg(
(Config.options.background.wallpaperPath || Translation.tr("none")).split("/").pop())
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
elide: Text.ElideMiddle
}
// Content filter — which wallhaven categories the random pick may
// include. Persisted to config.json where the download script reads it.
StyledText {
Layout.fillWidth: true
text: Translation.tr("Allowed content")
color: Appearance.colors.colOnLayer1
font.pixelSize: Appearance.font.pixelSize.small
}
Flow {
Layout.fillWidth: true
spacing: 6
Repeater {
model: [
{ key: "sfw", label: Translation.tr("SFW") },
{ key: "sketchy", label: Translation.tr("Sketchy") },
{ key: "nsfw", label: Translation.tr("NSFW") }
]
RippleButton {
id: purityChip
required property var modelData
property bool on: modelData.key === "sfw" ? WallpaperDownload.puritySfw
: modelData.key === "sketchy" ? WallpaperDownload.puritySketchy
: WallpaperDownload.purityNsfw
padding: 8
buttonRadius: Appearance.rounding.small
toggled: on
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
onClicked: {
if (modelData.key === "sfw")
WallpaperDownload.puritySfw = !WallpaperDownload.puritySfw;
else if (modelData.key === "sketchy")
WallpaperDownload.puritySketchy = !WallpaperDownload.puritySketchy;
else
WallpaperDownload.purityNsfw = !WallpaperDownload.purityNsfw;
WallpaperDownload.savePurity();
}
contentItem: RowLayout {
spacing: 4
MaterialSymbol {
iconSize: 16
text: purityChip.on ? "check" : "add"
color: purityChip.on ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1
}
StyledText {
font.pixelSize: Appearance.font.pixelSize.small
text: modelData.label
color: purityChip.on ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1
}
}
}
}
}
// Download a fresh random wallpaper from wallhaven. Downloads land in
// ~/Pictures/Wallpapers (each a distinct wallhaven_<id> file), then
// apply through the shell like any picked image.
RippleButton {
Layout.fillWidth: true
padding: 10
buttonRadius: Appearance.rounding.small
enabled: !WallpaperDownload.downloading
onClicked: WallpaperDownload.download()
contentItem: RowLayout {
spacing: 8
MaterialSymbol {
iconSize: 20
text: WallpaperDownload.downloading ? "hourglass_top" : "cloud_download"
color: Appearance.colors.colOnLayer1
}
StyledText {
Layout.fillWidth: true
font.pixelSize: Appearance.font.pixelSize.normal
text: WallpaperDownload.downloading
? Translation.tr("Downloading…")
: Translation.tr("Download random wallpaper")
color: Appearance.colors.colOnLayer1
}
}
}
StyledText {
visible: WallpaperDownload.lastError.length > 0
Layout.fillWidth: true
text: WallpaperDownload.lastError
color: Appearance.colors.colError
font.pixelSize: Appearance.font.pixelSize.smaller
wrapMode: Text.WordWrap
}
// Quick directory buttons
Flow {
Layout.fillWidth: true
spacing: 6
Repeater {
model: quickDirs
RippleButton {
required property var modelData
property bool isCurrent: currentDir === modelData.path
padding: 8
buttonRadius: Appearance.rounding.small
toggled: isCurrent
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
onClicked: {
currentDir = modelData.path;
}
contentItem: RowLayout {
spacing: 4
MaterialSymbol {
iconSize: 16
text: modelData.icon
color: isCurrent ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1
}
StyledText {
font.pixelSize: Appearance.font.pixelSize.small
text: modelData.name
color: isCurrent ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1
}
}
}
}
}
// Current path display
StyledText {
Layout.fillWidth: true
text: currentDir.replace(homeDir, "~")
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
elide: Text.ElideMiddle
}
GridView {
id: grid
Layout.fillWidth: true
// Rows of ~3 across the phone width; height fits the model.
readonly property int cols: 3
cellWidth: Math.floor(width / cols)
cellHeight: Math.floor(cellWidth * 2) // portrait-ish tiles
implicitHeight: Math.ceil(folderModel.count / cols) * cellHeight
interactive: false // the page scrolls, not the grid
clip: true
model: FolderListModel {
id: folderModel
folder: "file://" + currentDir
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.webp", "*.avif"]
showDirs: true
showDotAndDotDot: false
showOnlyReadable: true
sortField: FolderListModel.Name
}
delegate: Item {
required property string filePath
required property string fileName
required property bool fileIsDir
width: grid.cellWidth
height: grid.cellHeight
Rectangle {
anchors.fill: parent
anchors.margins: 5
radius: Appearance.rounding.small
color: Appearance.colors.colLayer1
border.width: (!fileIsDir && Config.options.background.wallpaperPath === filePath) ? 3 : 0
border.color: Appearance.colors.colPrimary
clip: true
Image {
anchors.fill: parent
anchors.margins: 3
source: fileIsDir ? "" : "file://" + filePath
fillMode: Image.PreserveAspectCrop
asynchronous: true
sourceSize.width: 240
visible: !fileIsDir
}
// Directory indicator
Column {
anchors.centerIn: parent
visible: fileIsDir
spacing: 4
MaterialSymbol {
anchors.horizontalCenter: parent.horizontalCenter
iconSize: 32
text: "folder"
color: Appearance.colors.colPrimary
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
font.pixelSize: Appearance.font.pixelSize.smaller
text: fileName
color: Appearance.colors.colOnLayer1
elide: Text.ElideRight
width: grid.cellWidth - 16
horizontalAlignment: Text.AlignHCenter
}
}
MouseArea {
anchors.fill: parent
onClicked: {
if (fileIsDir) {
currentDir = filePath;
} else {
// Shell process owns selection + theming.
Wallpapers.apply(filePath);
Config.options.background.wallpaperPath = filePath;
}
}
}
}
}
}
StyledText {
visible: folderModel.count === 0
Layout.fillWidth: true
text: Translation.tr("No images in %1").arg(currentDir.replace(homeDir, "~"))
color: Appearance.colors.colSubtext
wrapMode: Text.WordWrap
}
}
}

View file

@ -0,0 +1,21 @@
About 1.0 About.qml
AdvancedConfig 1.0 AdvancedConfig.qml
BackgroundConfig 1.0 BackgroundConfig.qml
BarConfig 1.0 BarConfig.qml
DeviceConfig 1.0 DeviceConfig.qml
DisplayConfig 1.0 DisplayConfig.qml
DockConfig 1.0 DockConfig.qml
GeneralConfig 1.0 GeneralConfig.qml
IdleConfig 1.0 IdleConfig.qml
InterfaceConfig 1.0 InterfaceConfig.qml
KeyboardConfig 1.0 KeyboardConfig.qml
LockConfig 1.0 LockConfig.qml
NavigationConfig 1.0 NavigationConfig.qml
NetworkConfig 1.0 NetworkConfig.qml
OverviewConfig 1.0 OverviewConfig.qml
QuickConfig 1.0 QuickConfig.qml
ServicesConfig 1.0 ServicesConfig.qml
SettingsHome 1.0 SettingsHome.qml
SpeechConfig 1.0 SpeechConfig.qml
SoundConfig 1.0 SoundConfig.qml
WallpaperConfig 1.0 WallpaperConfig.qml