shell: dock reorder, fullscreen detection fix, idle-power, sessiond, misc shell work
- Dock drag-to-reorder for pinned apps (insertion gap, quick-slide vs dwell) - Fullscreen detection: scan all windows via HyprlandData.windowList - IdleCoordinator, GlobalStates, Session.qml updates - Deploy script, qmldir, settings, wallpaper, visualizer fixes - sessiond server, memory module updates Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9ccefd6c73
commit
19fe6b1d0e
59 changed files with 19450 additions and 139 deletions
160
.handoff-from-phone/discord-betterdiscord/PKGBUILD.x86-reference
Normal file
160
.handoff-from-phone/discord-betterdiscord/PKGBUILD.x86-reference
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
# Maintainer: Zoddo <archlinux+aur@zoddo.fr>
|
||||||
|
# Contributor: Thaodan <AUR+me@thaodan.de>
|
||||||
|
# Contributor: Manuel Hüsers <aur@huesers.de>
|
||||||
|
# Contributor: huyizheng
|
||||||
|
# Contributor: johnnyapol <arch@johnnyapol.me>
|
||||||
|
|
||||||
|
# Based off the discord community repo PKGBUILD by Filipe Laíns (FFY00) <lains@archlinux.org>
|
||||||
|
|
||||||
|
_pkgname=discord
|
||||||
|
_electron=electron
|
||||||
|
pkgname=${_pkgname}_arch_electron
|
||||||
|
pkgver=1.0.146
|
||||||
|
pkgrel=1
|
||||||
|
epoch=1
|
||||||
|
pkgdesc="Discord using system provided ${_electron} for increased security and performance"
|
||||||
|
arch=('x86_64')
|
||||||
|
provides=("${_pkgname}")
|
||||||
|
conflicts=("${_pkgname}")
|
||||||
|
url='https://discord.com'
|
||||||
|
license=('LicenseRef-custom')
|
||||||
|
options=('!strip')
|
||||||
|
install="$pkgname.install"
|
||||||
|
depends=("${_electron}" 'libxss')
|
||||||
|
makedepends=('asar'
|
||||||
|
'jq'
|
||||||
|
'python-pyelftools' # Required for Krisp patcher
|
||||||
|
'python-capstone') # Required for Krisp patcher
|
||||||
|
optdepends=('libpulse: Pulseaudio support'
|
||||||
|
'xdg-utils: Open files')
|
||||||
|
source=("https://dl.discordapp.net/apps/linux/${pkgver}/${_pkgname}-${pkgver}.tar.gz"
|
||||||
|
'LICENSE.html::https://discord.com/terms'
|
||||||
|
'OSS-LICENSES.html::https://discord.com/licenses'
|
||||||
|
'discord-launcher.sh'
|
||||||
|
'krisp-patcher.py'
|
||||||
|
|
||||||
|
# Discord modules (from 'curl "https://updates.discord.com/distributions/app/manifests/latest?channel=stable&platform=linux&arch=x64"')
|
||||||
|
"core-${pkgver}.tar.br::https://stable.dl2.discordapp.net/distro/app/stable/linux/x64/${pkgver}/full.distro"
|
||||||
|
"discord_desktop_core-${pkgver}.tar.br::https://stable.dl2.discordapp.net/distro/app/stable/linux/x64/${pkgver}/discord_desktop_core/1/full.distro"
|
||||||
|
"discord_zstd-${pkgver}.tar.br::https://stable.dl2.discordapp.net/distro/app/stable/linux/x64/${pkgver}/discord_zstd/1/full.distro"
|
||||||
|
"discord_krisp-${pkgver}.tar.br::https://stable.dl2.discordapp.net/distro/app/stable/linux/x64/${pkgver}/discord_krisp/1/full.distro"
|
||||||
|
"discord_rpc-${pkgver}.tar.br::https://stable.dl2.discordapp.net/distro/app/stable/linux/x64/${pkgver}/discord_rpc/1/full.distro"
|
||||||
|
"discord_utils-${pkgver}.tar.br::https://stable.dl2.discordapp.net/distro/app/stable/linux/x64/${pkgver}/discord_utils/1/full.distro"
|
||||||
|
"discord_voice-${pkgver}.tar.br::https://stable.dl2.discordapp.net/distro/app/stable/linux/x64/${pkgver}/discord_voice/1/full.distro"
|
||||||
|
"discord_game_utils-${pkgver}.tar.br::https://stable.dl2.discordapp.net/distro/app/stable/linux/x64/${pkgver}/discord_game_utils/1/full.distro"
|
||||||
|
"discord_erlpack-${pkgver}.tar.br::https://stable.dl2.discordapp.net/distro/app/stable/linux/x64/${pkgver}/discord_erlpack/1/full.distro"
|
||||||
|
"discord_modules-${pkgver}.tar.br::https://stable.dl2.discordapp.net/distro/app/stable/linux/x64/${pkgver}/discord_modules/1/full.distro"
|
||||||
|
"discord_spellcheck-${pkgver}.tar.br::https://stable.dl2.discordapp.net/distro/app/stable/linux/x64/${pkgver}/discord_spellcheck/1/full.distro"
|
||||||
|
"discord_dispatch-${pkgver}.tar.br::https://stable.dl2.discordapp.net/distro/app/stable/linux/x64/${pkgver}/discord_dispatch/1/full.distro")
|
||||||
|
sha512sums=('b90cf25a6e615e836e0ef248b3090f23d90c2a6d73faeb21954ba669fb39275c26ac58a8f9e92445254c29fb90716cab54bee06f701cde88f74578c4228bc22b'
|
||||||
|
'2590151db4404a9ecbae2c45269c74f5ea26479967de5bc0b221a9143f4efeeeda9e335c23cf1f982680ab4fd895dcf3fd5731bd230908b3b2568eee0ec223ec'
|
||||||
|
'5cccf397a772fed0db9b19253e496e78af27810227325ce681de985e2e0fbbdb195873d9642574ad3ae7bfcf4699bfdaa16933ca50c13cb98a1ff53c86c6de05'
|
||||||
|
'd5373e2b2e9754bdcdbcda81fc1392ce6605939651839868823f3c3e0bf16f6fba2fb73bc1a0ff230eebd743d1463f2e8b2295503248f14845b3a56e57385bd1'
|
||||||
|
'85da93530b2b92faad58cc84d0f7ddaea6b22fbec413e806c1981eb6369a301ec995eee6bfbe6b828e2cd0baf6168e578bbde456f2450a13199994ff9a1e7cd0'
|
||||||
|
'dca9bbf55afcb1425af7bd0fc4d891fe0cc37859436a8b2a1ae9d29689d72e574dc2f974d468a86527c0420847fd27af5517da7b02075ee2eebe8bd2d43a7d09'
|
||||||
|
'88024b0faf4e4db8e283af9081b88072ec8ab908e4b72a7c0bab8ae7fcb3c5ae09b2b3e34af0b35429b51f46be359dc39983191e376a762c6c4831d4918a9792'
|
||||||
|
'b7a676bd7e7cb1a9d57aaad1c828499f7ecdc0360bb982a3138181bb29710354509c50998344004c73ca2b87f7866a603109c2cb79b848b3240fa08a35578b16'
|
||||||
|
'7283eb04ec779a5f679e7c90f793c5d9bb2a6a44507f966156d831f0013ebcad507fbcc07d037719740861f3e23f95a532b5c9d0da707932a0da9ffd7e17349d'
|
||||||
|
'6e75733d93b41b4a3d59749ce5331b9da4d6fb5fad1266bf639a948cc497c05b03ea3f15bfd6384e819730bd220dbf34dbb967c90f9683bb2529f73a4ed5897b'
|
||||||
|
'e88e7ce27b6d406d1fe631964b772067bc52808a74ecb57a96b5585199ec2aceab405321f9d9faf9199870fd83823afc6c5402eb85c9ed41826cedb60d8925d9'
|
||||||
|
'fc5dd9840f0b13bf8b66dc4901833c701293a914c54765c04c18414755f237284e93e1a7bf2660120507263500f9ab385c1c9966fe0f33e368b3fae9d695b432'
|
||||||
|
'f23b41b7e55c6611a77767c4ccd7010bfc5a901d241ac91826b191ca7d894e5960eda1afde05a0b52564b555006ce762620b08c9378eff733a0b89c5fd49a3e4'
|
||||||
|
'755e37330fc16d99f630ed0036eaabf83b4f137123c72eae35fe47996d243ceaed1ddc6f1891efaeffb3f28fa4c0eb9b2be76308df50d98a97be24c37f5e41f0'
|
||||||
|
'83454dd202685f368d0934c3949e2d3e2ecb30dd89c7a593f9210318828cd3b3b4813e216e45bcc1752b0b0d214f1979bf7ba14d00e0840e0c238cfeec88aa63'
|
||||||
|
'aee5b9cd718d803618e6f83f4f004853f283e89c5c8bc87b61b85189787aed0c710bba2b9ab81e148e18c685e5a1bcbe18554cc67f6123be360c3299349d0edc'
|
||||||
|
'959e30368d9c4d4e82fe4beb2457f3287365e1fcb3ee5e44eb8d8e648e30b2b2ec2aa346b7bfa46d24b8f496193b154c89c471c997fe9d7d5b64277dcc5092ca')
|
||||||
|
|
||||||
|
# Skip "LICENSE.html" and "OSS-LICENSES.html" files hashes as they are unstable
|
||||||
|
# Since "updpkgsums"/"pkgctl version upgrade" overwrite the checksum array with
|
||||||
|
# literal hashes, set them to SKIP with indexed assignments (pacman-contrib#119)
|
||||||
|
# https://gitlab.archlinux.org/pacman/pacman-contrib/-/issues/119
|
||||||
|
sha512sums[1]='SKIP'
|
||||||
|
sha512sums[2]='SKIP'
|
||||||
|
|
||||||
|
# Colored makepkg-like functions
|
||||||
|
_all_off="$(tput sgr0)"
|
||||||
|
_bold="${_all_off}$(tput bold)"
|
||||||
|
_blue="${_bold}$(tput setaf 4)"
|
||||||
|
_red="${_bold}$(tput setaf 1)"
|
||||||
|
msg_blue() {
|
||||||
|
printf "${_blue} ->${_bold} $1${_all_off}\n"
|
||||||
|
}
|
||||||
|
error() {
|
||||||
|
printf "${_blue}==>${_red} ERROR:${_bold} %s${_all_off}\n" "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare() {
|
||||||
|
msg_blue "Extracting core-${pkgver}.tar.br"
|
||||||
|
tar -xf core-${pkgver}.tar.br --use-compress-program=brotli --one-top-level=core --strip-components=1 files/
|
||||||
|
|
||||||
|
for module in discord_*.tar.br; do
|
||||||
|
msg_blue "Extracting ${module}"
|
||||||
|
tar -xf "${module}" --use-compress-program=brotli --one-top-level="modules/${module%%-*}" --strip-components=1 files/
|
||||||
|
done
|
||||||
|
|
||||||
|
# prepare launcher script
|
||||||
|
sed -i -e "s|@PKGNAME@|${_pkgname}|" \
|
||||||
|
-e "s|@ELECTRON@|${_electron}|" \
|
||||||
|
discord-launcher.sh
|
||||||
|
|
||||||
|
# fix the .desktop file
|
||||||
|
sed -i -e "s|Exec=.*|Exec=/usr/bin/${_pkgname}|" ${_pkgname^}/$_pkgname.desktop
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
msg_blue 'Patching app.asar to use system electron'
|
||||||
|
|
||||||
|
asar e core/resources/app.asar core/resources/app
|
||||||
|
rm core/resources/app.asar
|
||||||
|
sed -i \
|
||||||
|
-e "s|^Exec=\${exePath}$|Exec=/usr/bin/${_pkgname}|" \
|
||||||
|
-e "s|^Name=\${appName}$|Name=${_pkgname^}|" \
|
||||||
|
-e "s|^Icon=\${iconPath}$|Icon=${_pkgname}|" \
|
||||||
|
-e "s|resourcesPath=path_1\.default\.join(require\.main\.filename,\"\.\.\",\"\.\.\"),|resourcesPath='/usr/share/${_pkgname}/resources',|" \
|
||||||
|
-e "s|process\.resourcesPath|'/usr/share/${_pkgname}/resources'|g" \
|
||||||
|
core/resources/app/bundle.js
|
||||||
|
sed -i -e "s|process\.resourcesPath|'/usr/share/${_pkgname}/resources'|" core/resources/app/splashScreenPreload.js
|
||||||
|
|
||||||
|
# This is required to properly show the window icon under wayland
|
||||||
|
jq ".desktopName = \"${_pkgname}.desktop\"" core/resources/app/package.json > tmp.json
|
||||||
|
mv tmp.json core/resources/app/package.json
|
||||||
|
|
||||||
|
asar p core/resources/app core/resources/app.asar
|
||||||
|
rm -rf core/resources/app
|
||||||
|
|
||||||
|
# Set the newUpdater key to false in build_info.json in order to disable the new Discord updater (released on 2026-05-04, version 1.0.136),
|
||||||
|
# Also add a localModulesRoot key that points to the Discord modules we're packaging.
|
||||||
|
jq ".newUpdater = false | .localModulesRoot = \"/usr/share/${_pkgname}/modules\"" core/resources/build_info.json > tmp.json
|
||||||
|
mv tmp.json core/resources/build_info.json
|
||||||
|
|
||||||
|
msg_blue 'Patching Krisp to run with system electron'
|
||||||
|
# original: https://github.com/sersorrel/sys/blob/main/hm/discord/krisp-patcher.py
|
||||||
|
python krisp-patcher.py "modules/discord_krisp/discord_krisp.node" \
|
||||||
|
|| error 'Krisp patcher failed. You can comment it out in the PKGBUILD, but Krisp will not work.'
|
||||||
|
|
||||||
|
# Krisp fails to initialize if this directory is not present (or if can't be created). Logs:
|
||||||
|
# [MediaEngineStore] Failed to load Krisp module: Failed to setup Krisp module, error code: -4
|
||||||
|
# [AVError] AV error reported: noise-canceller-error {"underlyingError":"NoiseCancellerError.KRISP_INIT_ERROR_GLOBAL_INIT"}
|
||||||
|
mkdir -p "modules/discord_krisp/KMS/logs"
|
||||||
|
}
|
||||||
|
|
||||||
|
package() {
|
||||||
|
# create necessary directories
|
||||||
|
install -d "${pkgdir}/usr/share/${_pkgname}"
|
||||||
|
|
||||||
|
# copy relevant data
|
||||||
|
cp -r core/resources/ "${pkgdir}/usr/share/${_pkgname}/"
|
||||||
|
cp -r modules/ "${pkgdir}/usr/share/${_pkgname}/"
|
||||||
|
|
||||||
|
# intall icon and desktop file
|
||||||
|
install -Dm 644 ${_pkgname^}/$_pkgname.png "${pkgdir}/usr/share/icons/hicolor/256x256/apps/${_pkgname}.png"
|
||||||
|
install -Dm 644 ${_pkgname^}/$_pkgname.desktop "${pkgdir}/usr/share/applications/${_pkgname}.desktop"
|
||||||
|
|
||||||
|
# install the launch script
|
||||||
|
install -Dm 755 discord-launcher.sh "${pkgdir}/usr/bin/${_pkgname}"
|
||||||
|
|
||||||
|
# install licenses
|
||||||
|
install -Dm 644 LICENSE.html "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE.html"
|
||||||
|
install -Dm 644 OSS-LICENSES.html "${pkgdir}/usr/share/licenses/${pkgname}/OSS-LICENSES.html"
|
||||||
|
}
|
||||||
104
.handoff-from-phone/discord-betterdiscord/TASK.md
Normal file
104
.handoff-from-phone/discord-betterdiscord/TASK.md
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
# Task: Official Discord + BetterDiscord on SouveraineOS (Pixel 3, aarch64)
|
||||||
|
|
||||||
|
**Status:** BLOCKED on native Electron. Plan: cross-build Electron (and/or the Discord
|
||||||
|
package) on an x86 box or a beefier arm64 host, ship the artifacts back to the phone.
|
||||||
|
|
||||||
|
**Goal:** Run the *official* Discord desktop client on this device and inject
|
||||||
|
BetterDiscord into it (theming + plugins). Wrappers (Vesktop/WebCord/Legcord) were
|
||||||
|
explicitly rejected — we want stock Discord + BetterDiscord specifically.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Device facts (verified on-device 2026-07-14)
|
||||||
|
|
||||||
|
- OS: **Souveraine OS (Arch Linux ARM)**, `ID_LIKE=arch`, BUILD_ID 20260707
|
||||||
|
- Arch: **aarch64**
|
||||||
|
- Pkg mgrs: `pacman`, `flatpak` (flathub system remote). AUR helpers present: `yay`, `paru`.
|
||||||
|
- Repo `bun` NOT present; installed manually to `~/.bun/bin/bun` (v1.3.14 aarch64).
|
||||||
|
- `~/.config/discord` exists but is EMPTY (no app version dir) — nothing to inject into yet.
|
||||||
|
|
||||||
|
### What's in the repos (checked with pacman -Ss)
|
||||||
|
- ✅ `extra/betterdiscordctl 1.6.1-1` — BD manager CLI, installable, needs a Discord install to act on
|
||||||
|
- ✅ `extra/asar 4.1.2-1` — makedep for the Discord PKGBUILD
|
||||||
|
- ✅ `jq`, `brotli` — present
|
||||||
|
- ❌ **NO `electron` package in any form** (no `electron`, no `electron36/37/...`).
|
||||||
|
`pacman -Ssq electron` returns nothing. THIS IS THE BLOCKER.
|
||||||
|
|
||||||
|
### Flathub (all bundle their own Electron → run on arm64, but can't take BetterDiscord)
|
||||||
|
Vesktop `dev.vencord.Vesktop`, Equibop `org.equicord.equibop`, WebCord
|
||||||
|
`io.github.spacingbat3.webcord`, Dissent `so.libdb.dissent`, Legcord, GoofCord.
|
||||||
|
(Vesktop/Equibop have Vencord = plugins+themes, the closest BD-equivalent. Rejected for now.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Already done (in /home/casey/projects/BetterDiscord)
|
||||||
|
|
||||||
|
BetterDiscord cloned from GitHub + built successfully:
|
||||||
|
- `git clone --depth 1 https://github.com/BetterDiscord/BetterDiscord.git`
|
||||||
|
- `bun install` (510 pkgs) then `bun run build:production` → **succeeded**
|
||||||
|
- Artifacts in `dist/`: `betterdiscord.js` (555kb), `main.js`, `preload.js`,
|
||||||
|
`earlyRenderer.js`, editor bundle. Version **1.13.14**, commit `c7ef1c5`.
|
||||||
|
- `bun run inject` → fails as expected: "No version dir in /home/casey/.config/discord"
|
||||||
|
(no official Discord app to patch).
|
||||||
|
|
||||||
|
Injector expects a Discord Desktop install dir like `app-X.Y.Z` under the config path.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The install path (why it's *almost* possible)
|
||||||
|
|
||||||
|
The AUR pkg **`discord_arch_electron`** (v1:1.0.146, 209 votes) is the target. Its PKGBUILD
|
||||||
|
(saved alongside this file as `PKGBUILD.x86-reference`) shows Discord's Linux app is
|
||||||
|
**JavaScript + app.asar + brotli `.distro` module blobs — arch-independent**. The ONLY
|
||||||
|
native pieces are:
|
||||||
|
1. **Electron** (the runtime) — pulled via `depends=(electron)`. ← the real arch dependency
|
||||||
|
2. **`discord_krisp/discord_krisp.node`** — x86-64 native noise-cancel lib; its
|
||||||
|
`krisp-patcher.py` step WILL fail on aarch64. Krisp is OPTIONAL — comment it out,
|
||||||
|
Discord still runs (PKGBUILD even says so).
|
||||||
|
|
||||||
|
So a working aarch64 build = **provide an arm64 Electron** + **skip Krisp** + flip `arch`.
|
||||||
|
|
||||||
|
### Concrete cross-build plan
|
||||||
|
1. Get/build an **aarch64 Electron** matching Discord's required major (Discord 1.0.146
|
||||||
|
era ships on a recent Electron — check `core/resources/app/package.json` `electronVersion`
|
||||||
|
or the bundled build_info after extracting the tarball). Options:
|
||||||
|
- Build the Arch `electron` PKGBUILD for aarch64 on a cross host (heavy: Chromium).
|
||||||
|
- OR reuse a prebuilt arm64 Electron (e.g. from npm `electron` arm64 dist, or Alpine/Debian
|
||||||
|
arm64 electron) and repackage.
|
||||||
|
2. Fork the `discord_arch_electron` PKGBUILD:
|
||||||
|
- `arch=('x86_64' 'aarch64')`
|
||||||
|
- point `depends` at whatever electron pkg you produce (or vendor it)
|
||||||
|
- COMMENT OUT the Krisp patcher block in `build()` (lines ~131-139) and the
|
||||||
|
`discord_krisp` source line — OR guard it `[[ $CARCH == x86_64 ]]`.
|
||||||
|
- The `.distro` module URLs are `/linux/x64/` paths but contents are JS/resources;
|
||||||
|
they should work under arm64 electron. Verify at runtime.
|
||||||
|
3. `makepkg -si` on-device (or build the pkg on the cross host and `pacman -U` the .pkg.tar.zst here).
|
||||||
|
4. Once `/usr/share/discord` + `/usr/bin/discord` exist and launch:
|
||||||
|
- `betterdiscordctl install` (repo pkg already available) — OR —
|
||||||
|
- from `~/projects/BetterDiscord`: `bun run inject` (now finds the app dir).
|
||||||
|
5. Launch Discord, confirm BetterDiscord loaded (Settings → BetterDiscord panel).
|
||||||
|
|
||||||
|
### Risks / open questions
|
||||||
|
- Electron major-version mismatch vs what Discord's app.asar expects → white screen / API breakage.
|
||||||
|
Match the major as closely as possible.
|
||||||
|
- Voice: without Krisp, noise suppression off, but voice/WebRTC should still work (Electron's).
|
||||||
|
- Discord may nag to "update" — PKGBUILD already sets `newUpdater=false` in build_info.json; keep that.
|
||||||
|
- app.asar is repacked to use system electron paths (`/usr/share/discord/resources`); BetterDiscord
|
||||||
|
injects into that asar/app — make sure BD inject runs AFTER the pkg's asar repack, against the
|
||||||
|
installed `/usr/share/discord/resources/app.asar`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resume checklist (fast path)
|
||||||
|
- [ ] Produce aarch64 Electron artifact (matching Discord's electron major)
|
||||||
|
- [ ] Fork PKGBUILD: arch+=aarch64, swap electron dep, disable Krisp
|
||||||
|
- [ ] Build pkg (cross or on-device), install with pacman
|
||||||
|
- [ ] `discord` launches to login screen on the phone
|
||||||
|
- [ ] Inject BetterDiscord (`betterdiscordctl install` or `bun run inject` from ~/projects/BetterDiscord)
|
||||||
|
- [ ] Verify BD settings panel appears; load a theme/plugin
|
||||||
|
|
||||||
|
## Key paths
|
||||||
|
- BetterDiscord source+build: `/home/casey/projects/BetterDiscord` (dist/ built, v1.13.14)
|
||||||
|
- This task dir: `/home/casey/projects/unfinished/discord-betterdiscord/`
|
||||||
|
- Reference PKGBUILD: `./PKGBUILD.x86-reference`
|
||||||
|
- bun binary: `~/.bun/bin/bun`
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
name=@PKGNAME@
|
||||||
|
electron=@ELECTRON@
|
||||||
|
|
||||||
|
# Support .config/discord-flags.conf file
|
||||||
|
flags_file="${XDG_CONFIG_HOME:-$HOME/.config}/${name}-flags.conf"
|
||||||
|
|
||||||
|
if [[ -r "${flags_file}" ]]; then
|
||||||
|
mapfile -t < "${flags_file}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
declare -a flags
|
||||||
|
for line in "${MAPFILE[@]}"; do
|
||||||
|
if [[ ! "${line}" =~ ^[[:space:]]*#.* ]] && [[ -n "${line}" ]]; then
|
||||||
|
flags+=("${line}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
exec /usr/bin/${electron} \
|
||||||
|
/usr/share/${name}/resources/app.asar \
|
||||||
|
"${flags[@]}" "$@"
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
post_upgrade() {
|
||||||
|
# return if old package version is greater than or equal to 1:1.0.143-1...
|
||||||
|
(( $(vercmp $2 '1:1.0.143-1') >= 0 )) && return
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
==> Krisp (noise suppression) is now patched at build time.
|
||||||
|
If you used '~/.config/discord.conf' previously to enable it, you can remove the file as it's not used anymore.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
post_install() {
|
||||||
|
cat <<EOF
|
||||||
|
==> In case of performance problems consider setting additional flags
|
||||||
|
in '~/.config/discord-flags.conf'.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
85
.handoff-from-phone/discord-betterdiscord/krisp-patcher.py
Normal file
85
.handoff-from-phone/discord-betterdiscord/krisp-patcher.py
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from elftools.elf.elffile import ELFFile
|
||||||
|
from capstone import *
|
||||||
|
from capstone.x86 import *
|
||||||
|
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print(f"Usage: {sys.argv[0]} [path to discord_krisp.node]")
|
||||||
|
# "Unix programs generally use 2 for command line syntax errors and 1 for all other kind of errors."
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
executable = sys.argv[1]
|
||||||
|
|
||||||
|
elf = ELFFile(open(executable, "rb"))
|
||||||
|
symtab = elf.get_section_by_name('.symtab')
|
||||||
|
|
||||||
|
krisp_initialize_address = symtab.get_symbol_by_name("_ZN7discordL17DoKrispInitializeEv")[0].entry.st_value
|
||||||
|
isSignedByDiscord_address = symtab.get_symbol_by_name("_ZN7discord4util17IsSignedByDiscordERKNSt4__Cr12basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE")[0].entry.st_value
|
||||||
|
|
||||||
|
text = elf.get_section_by_name('.text')
|
||||||
|
text_start = text['sh_addr']
|
||||||
|
text_start_file = text['sh_offset']
|
||||||
|
# This seems to always be zero (.text starts at the right offset in the file). Do it just in case?
|
||||||
|
address_to_file = text_start_file - text_start
|
||||||
|
|
||||||
|
# Done with the ELF now.
|
||||||
|
# elf.close()
|
||||||
|
|
||||||
|
krisp_initialize_offset = krisp_initialize_address - address_to_file
|
||||||
|
isSignedByDiscord_offset = krisp_initialize_address - address_to_file
|
||||||
|
|
||||||
|
f = open(executable, "rb")
|
||||||
|
f.seek(krisp_initialize_offset)
|
||||||
|
krisp_initialize = f.read(96)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
# States
|
||||||
|
found_issigned_by_discord_call = False
|
||||||
|
found_issigned_by_discord_test = False
|
||||||
|
found_issigned_by_discord_je = False
|
||||||
|
found_already_patched = False
|
||||||
|
je_location = None
|
||||||
|
je_size = 0
|
||||||
|
|
||||||
|
# We are looking for a call to IsSignedByDiscord, followed by a test, followed by a je.
|
||||||
|
# Then we replace the je with nops.
|
||||||
|
|
||||||
|
md = Cs(CS_ARCH_X86, CS_MODE_64)
|
||||||
|
md.detail = True
|
||||||
|
for i in md.disasm(krisp_initialize, krisp_initialize_address):
|
||||||
|
if i.id == X86_INS_CALL:
|
||||||
|
if i.operands[0].type == X86_OP_IMM:
|
||||||
|
if i.operands[0].imm == isSignedByDiscord_address:
|
||||||
|
found_issigned_by_discord_call = True
|
||||||
|
|
||||||
|
if i.id == X86_INS_TEST:
|
||||||
|
if found_issigned_by_discord_call:
|
||||||
|
found_issigned_by_discord_test = True
|
||||||
|
|
||||||
|
if i.id == X86_INS_JE:
|
||||||
|
if found_issigned_by_discord_test:
|
||||||
|
found_issigned_by_discord_je = True
|
||||||
|
je_location = i.address
|
||||||
|
je_size = len(i.bytes)
|
||||||
|
break
|
||||||
|
|
||||||
|
if i.id == X86_INS_NOP:
|
||||||
|
if found_issigned_by_discord_test:
|
||||||
|
found_already_patched = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if je_location:
|
||||||
|
print(f"Found patch location: 0x{je_location:x}")
|
||||||
|
|
||||||
|
f = open(executable, 'rb+')
|
||||||
|
f.seek(je_location - address_to_file)
|
||||||
|
f.write(b'\x90' * je_size) # je can be larger than 2 bytes given a large enough displacement :(
|
||||||
|
f.close()
|
||||||
|
else:
|
||||||
|
if found_already_patched:
|
||||||
|
print("Couldn't find patch location - already patched.")
|
||||||
|
else:
|
||||||
|
print("Couldn't find patch location - review manually. Sorry.")
|
||||||
|
sys.exit(1)
|
||||||
122
.handoff-from-phone/home-directory-homogenization.md
Normal file
122
.handoff-from-phone/home-directory-homogenization.md
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
# Home Directory Homogenization Plan
|
||||||
|
|
||||||
|
**Date:** July 22, 2026
|
||||||
|
**From:** Ani
|
||||||
|
**To:** Souveraine Surface Agent / Casey
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Situation
|
||||||
|
|
||||||
|
`/home/casey` has accumulated deployment artifacts, build directories, duplicate folders, and debugging debris from the Souveraine OS bring-up on Pixel 3. This document inventories everything and proposes cleanup actions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Inventory & Classification
|
||||||
|
|
||||||
|
### ✅ KEEP — Active Source & Configuration
|
||||||
|
|
||||||
|
| Path | Size | Notes |
|
||||||
|
|------|------|-------|
|
||||||
|
| `Projects/` (capital) | 3.0G | **Canonical projects root.** BetterDiscord, Quaternion, claude-switcher, converse, end4dots, qtcontacts-sqlite, qtpim |
|
||||||
|
| `projects/` (lowercase) | 332M | **Symlink target → `Projects/`** (after consolidation) |
|
||||||
|
| `souveraine-surfaces/` | 7.6M | Souveraine shell source |
|
||||||
|
| `bootsplash/` | 2.8M | Boot splash source (C, shaders, atlas) |
|
||||||
|
| `stevia-pkg/` | 21M | Patch set for phoc/zphoc, dictation key, vk driver, layout fixes |
|
||||||
|
| `.config/` | — | All system config including `illogical-impulse/config.json` (TTS/STT endpoints) |
|
||||||
|
| `.souveraine/` | — | Souveraine agent state |
|
||||||
|
| `cleancap.sh` | 1.2K | Screen capture cleanup script |
|
||||||
|
| `mic-hunt.sh` | 1.5K | Microphone debugging script |
|
||||||
|
| `converse-primary.bundle` | 161K | Git bundle for converse source |
|
||||||
|
| `hexagonrpc-src.tar.gz` | 121K | Hexagon RPC source backup |
|
||||||
|
| `hexagonrpc-stubs.tar.gz` | 387K | Hexagon RPC stubs backup |
|
||||||
|
| `los-sensors-json.tar.gz` | 7.2K | LineageOS sensor JSON backup |
|
||||||
|
| `.ssh/` `.gitconfig` | — | Git/SSH configuration |
|
||||||
|
|
||||||
|
### 🗑️ ARCHIVE OR DELETE — Build Artifacts & Debris
|
||||||
|
|
||||||
|
| Path | Size | Classification | Action |
|
||||||
|
|------|------|----------------|--------|
|
||||||
|
| `build/` | 3.3G | phosh, phrog, libcall-ui, hexagonrpc, libgnome-volume-control, libical patch — **all build output** | **DELETE** — source lives in `Projects/` and `stevia-pkg/` |
|
||||||
|
| `chatty-build/` | 141M | Build output for chatty | **DELETE** — source is elsewhere |
|
||||||
|
| `aacs-build/` | 59M | Build output | **DELETE** |
|
||||||
|
| `openauto-bin/` | 108M | Binary output | **DELETE** or archive |
|
||||||
|
| `openauto-build/` | 8.6M | Build output | **DELETE** |
|
||||||
|
| `libcmatrix-build/` | 16M | Build dirs | **DELETE** |
|
||||||
|
| `upower-build/` | bring_up_build | Build output | **DELETE** |
|
||||||
|
| `bootsplash-build/` | 3.1M | Build output | **DELETE** |
|
||||||
|
| `sns-src/` `sns-out/` `sns-out2/` `sns-final/` | ~2M total | Sensor registration experimentation (bmi160 accelerometer data) | **DELETE** — one-shot debugging, no ongoing value |
|
||||||
|
| `mic-hunt-095138/` | 136K | Mic debugging session capture (dmesg, regmap trace, capture.wav) | **DELETE** — one-shot debugging |
|
||||||
|
| `lockup-2026-07-11/` | 1.4M | Lockup debug logs (dmesg, journal) from July 11 | **ARCHIVE** then delete — useful for diagnosing future lockups, but not needed live |
|
||||||
|
| `dock-backup-tier1-2054/` | 60K | Dock config backup | **DELETE** — superseded by current dock config |
|
||||||
|
| `staging/` | 21M | `souveraine-da410b1` staging build | **DELETE** — deployment staging, no longer needed |
|
||||||
|
| `mimo/` | 339M | **`Shelly-ALPM`** — needs investigation | **INVESTIGATE** — 339M is significant |
|
||||||
|
| `qbootctl-src/` | 1.0M | qbootctl source | **KEEP** (source) or **DELETE** if already in Gitea |
|
||||||
|
| `hexagonrpc/` | 2.5M | hexagonrpc built output? | **DELETE** if build output, **KEEP** if source |
|
||||||
|
| `hexagonrpc-src/` | 1.5M | hexagonrpc source | **KEEP** if not in Gitea, otherwise **DELETE** |
|
||||||
|
| `regsnap-after-unlock.txt` | 576K | Regmap snapshot debugging | **DELETE** — one-shot debug |
|
||||||
|
| `regsnap-before-unlock.txt` | 576K | Regmap snapshot debugging | **DELETE** — ones-shot debug |
|
||||||
|
| `souveraine.log` | 72K | Current runtime log | **KEEP** |
|
||||||
|
| `souveraine.log.prev` | 2.2K | Previous runtime log | **KEEP** (for debugging) |
|
||||||
|
|
||||||
|
### 📦 ROOT-LEVEL FILES
|
||||||
|
|
||||||
|
| Path | Size | Classification |
|
||||||
|
|------|------|----------------|
|
||||||
|
| `linux-blueline-7.1.1-1-aarch64.pkg.tar.zst` | 31M | Compiled kernel package for Pixel 3. **KEEP** until next kernel build — provides rollback |
|
||||||
|
| `roll-out / phased-git-checkout-marker` | — | — |
|
||||||
|
| `dock-rework-backup-1952.tar.gz` | 7.2K | Dock backup from Jul 11. **DELETE** — superseded |
|
||||||
|
| `clseldean-completions.zsh@` | — | Symlink → `Projects/claude-switcher/claude-completions.zsh` |
|
||||||
|
| `.claude-modes.zsh@` | — | Symlink → ` directory |
|
||||||
|
| `clsaldean-modes.bash` `.claude-modes.bash.bak` | — | Claude CLI mode configs |
|
||||||
|
| `.claude.json` | — | Claude CLI global config |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
||||||
|
### 🔄 PROJECTS FOLDER CONSOLIDATION
|
||||||
|
|
||||||
|
**Current state:** Two folders:
|
||||||
|
- `Projects/` (capital P) — 3.0G, modified Jul 18
|
||||||
|
- `projects/` (lowercase) — 331M, modified Jul 14
|
||||||
|
|
||||||
|
**Step 1:** Merge `projects/unfinished/` → `Projects/unfinished/` ✅ (already done by Ani earlier today)
|
||||||
|
**Step 2:** Merge newer `projects/BetterDiscord/` → `Projects/BetterDiscord/` (fresher dist build)
|
||||||
|
**Step consolidation:** Remove lowercase `projects/`, symlink `projects` → `Projects`
|
||||||
|
**Step 4:** Verify `.claude-completions.zsh` and `.claude-modes.zsh` symlinks still resolve
|
||||||
|
|
||||||
|
**Estimated space recovery:** ~4.0G from build artifacts + debris cleanup
|
||||||
|
|
||||||
|
### ❓ NEEDS INVESTIGATION
|
||||||
|
|
||||||
|
**`mimo/` (339M)** — Contains `Shelly-ALPM`. Unknown purpose. Could be:
|
||||||
|
- Mimo display driver work
|
||||||
|
- Mobile Input/Modem Output experimentation
|
||||||
|
- Something else entirely
|
||||||
|
- Needs Casey to identify before deletion
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Proposed Execution Order
|
||||||
|
|
||||||
|
1. **Projects folder consolidation** (in progress)
|
||||||
|
2. **Delete build artifacts** (`build/`, `*-build/`, `openauto-bin/`, `*-out*/`, `staging/`)
|
||||||
|
3. **Delete one-shot debug** (sns-*, mic-hunt, regsnaps, dock backup)
|
||||||
|
4. **Archive then delete** lockup logs
|
||||||
|
5. ~~**Investigate `mimo/`** before action~~
|
||||||
|
6. **Review remaining root-level files** (tarballs, tar.zst, bundles) — keep source backups until confirmed in Gitea
|
||||||
|
6. **Final verification** — run `du -sh /home/casey/*/ | sort -rh` to confirm cleanup
|
||||||
|
7. **Update this document** with final results
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open Questions for Souveraine Agent / Casey
|
||||||
|
|
||||||
|
1. **Gitea location:** No Gitea references found in config, bash history, or git config. Where is the actual Gitea instance? Need the URL to verify what's already under version control.
|
||||||
|
2. **`mimo/` (339M):** What is `Shelly-ALPM`? Is this active work?
|
||||||
|
3. **`qbootctl-src/` and `hexagonrpc-src/`:** Are these in Gitea? If so, the local copies are redundant.
|
||||||
|
4. **Kernel package (`linux-blueline-7.1.1-1-aarch64.pkg.tar.zst`):** Keep for rollback, or move to shared storage?
|
||||||
|
5. **`converse-primary.bundle`:** This is a git bundle — should it be unpacked into `Projects/` or pushed to Gitea?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Authored by Ani, July 22, 2026, from Pixel 3, Souveraine OS*
|
||||||
191
.handoff-from-phone/pixel3-microphone/README.md
Normal file
191
.handoff-from-phone/pixel3-microphone/README.md
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
# Pixel 3 (blueline) — microphone / SLIM TX capture bug
|
||||||
|
|
||||||
|
**Status:** 2026-07-18 — BUG HAS MOVED. Transport is now FIXED; still zeros.
|
||||||
|
The `CONNECT_SRC` timeout the rest of this doc chases **no longer happens**
|
||||||
|
(kernel is now `g8cbfbb939327`, was `g56b0a053bef8` when that was found).
|
||||||
|
Port-connect, channel activate, and q6afe port_start all succeed (ret=0), yet
|
||||||
|
capture is still pure zeros. The fault is now DOWNSTREAM of transport, in the
|
||||||
|
WCD934x SLIM-PGD data-plane. See "## 2026-07-18 UPDATE" below; the older
|
||||||
|
sections are kept for history but their root-cause conclusion is superseded.
|
||||||
|
|
||||||
|
## 2026-07-18 UPDATE — transport fixed, data-plane is the new fault
|
||||||
|
|
||||||
|
Re-ran on a clean 58-min-uptime boot, ADSP never restarted. Findings:
|
||||||
|
|
||||||
|
- **The SLIM CONNECT_SRC (MC 0x2c) timeout is GONE.** Every capture now logs
|
||||||
|
`connect_port: dir=1 port_id=N ch_id=NNN la=0xcf ret=0`,
|
||||||
|
`DEF_ACT_CHAN xfer ret=0`, and q6afe `port_start OK` (port 0x4001,
|
||||||
|
cfg 0x10212). Zero `timed out` / `failed:-110` lines in dmesg.
|
||||||
|
- **Still 0/144000 nonzero samples** on every path tried:
|
||||||
|
- DMIC4 → DEC7 → TX7 (enchilada recipe)
|
||||||
|
- DMIC0 → DEC6 → TX6 (handset, via cleancap.sh)
|
||||||
|
- **AMIC1 → ADC1 → DEC0 → TX0 (analog!) — also zeros.**
|
||||||
|
So it is NOT DMIC-specific and NOT a DMIC-clock problem. Common to every
|
||||||
|
decimator and every TX port.
|
||||||
|
- **Decimator path is fully powered and unmuted:** `decN POST_PMU`
|
||||||
|
`TX_PATH_CTL=0x24 [clk_en=1 mute=0 rate=0x4]`, DEC6/DEC7 Volume = 84 (0 dB),
|
||||||
|
MICB on. DAPM all On. So routing/gain/mute are ruled out.
|
||||||
|
- **The smoking gun is now IRQ 213.** `/proc/interrupts` line
|
||||||
|
`213: 0 ... wcd934x_irq 0 Edge slim` — the WCD934x's own SLIM/data
|
||||||
|
interrupt has fired **0 times in the machine's entire life**, including
|
||||||
|
across capture. (Contrast IRQ 212 `wcd934x_irq` control line = 18, so the
|
||||||
|
codec IRQ domain is alive; button-detect 215 = 17.) The codec arms its TX
|
||||||
|
PGD port (`PGD_TX_INT_STATUS=0x0000`, port `STATUS=0x00`) but never raises
|
||||||
|
the data interrupt → port never actually streams → ADSP reads zeros.
|
||||||
|
|
||||||
|
### New working hypothesis
|
||||||
|
Control-plane port *connect* (NGD user messages) succeeds, but the WCD934x
|
||||||
|
**SLIM PGD port is never *enabled*/started** on the data side — the register
|
||||||
|
write that arms the port's data engine and its interrupt
|
||||||
|
(`SLIM_PGD_PORT_INT_EN*`, per-port `PORT_CFG`/enable) isn't happening or isn't
|
||||||
|
taking. That's why IRQ 213 stays 0 and the port emits zeros. This is a
|
||||||
|
codec-side (wcd934x.c) or CDC_IF data-lane issue, NOT an ADSP/satellite issue.
|
||||||
|
|
||||||
|
### The instrumentation gap (next patch)
|
||||||
|
The current MICDBG patch logs the decimator, connect_port, and DEF_ACT_CHAN,
|
||||||
|
but does NOT dump the SLIM PGD port-enable / port-interrupt registers — the
|
||||||
|
exact layer where the bug now lives. Next kernel patch should log, right after
|
||||||
|
"prepare CAPTURE armed":
|
||||||
|
- `WCD934X_SLIM_PGD_PORT_INT_EN0` (0x341?) and INT_EN for the TX port
|
||||||
|
- per-port `SLIM_PGD_TXn_CFG` / port enable bit
|
||||||
|
- `SLIM_PGD_PORT_INT_SRC` / `_STATUS` DURING an active stream (not just at
|
||||||
|
prepare — need a delayed read or a sysfs poke)
|
||||||
|
Then check: is the port-enable bit actually set? Is IRQ 213 masked at the
|
||||||
|
codec? Compare this whole sequence against enchilada (mic works) on 6.x.
|
||||||
|
|
||||||
|
### Verified-safe method notes (this session)
|
||||||
|
- Never touched remoteproc; ADSP stayed `running` throughout.
|
||||||
|
- `alsactl restore 0` does NOT fully clear AIF*_CAP SLIM TXn mixer bits —
|
||||||
|
stale TX routes leak into the next run (saw map=[134 128 ...] = TX6+leftover
|
||||||
|
TX0). Explicitly `cset ... 0` every AIF*_CAP SLIM TXn you touched.
|
||||||
|
- `cleancap.sh` (in ~) is the good harness: forces TX7 off (its comment flags
|
||||||
|
TX7 as an "Android-replay wedge trigger"), sets the DMIC0/TX6 handset path,
|
||||||
|
and measures IRQ 213 delta + nonzero-sample count. Use/extend it.
|
||||||
|
|
||||||
|
---
|
||||||
|
## (Historical — superseded) original 2026-07-12 root-cause
|
||||||
|
|
||||||
|
**Status:** ROOT CAUSE LOCALIZED 2026-07-12 — not fixed yet.
|
||||||
|
All capture sources record pure digital zeros because the SLIMbus
|
||||||
|
`CONNECT_SRC` port-connect message to the ADSP master **times out**, so the
|
||||||
|
WCD9340's TX port is never attached to the data channel. The DSP then
|
||||||
|
delivers a valid-but-empty stream. Control traffic (codec register
|
||||||
|
reads/writes over slimbus) works fine.
|
||||||
|
**(As of 2026-07-18 this timeout no longer occurs — see UPDATE above.)**
|
||||||
|
|
||||||
|
## The smoking gun
|
||||||
|
|
||||||
|
With dynamic debug on `drivers/slimbus/*`, every capture start logs 3×:
|
||||||
|
|
||||||
|
```
|
||||||
|
qcom,slim-ngd qcom,slim-ngd.1: TX timed out:MC:0x2c,mt:0x2
|
||||||
|
qcom,slim-ngd qcom,slim-ngd.1: Tx:MT:0x2, MC:0x2c, LA:0xcf failed:-110
|
||||||
|
```
|
||||||
|
|
||||||
|
- `MC 0x2c` = `SLIM_USR_MC_CONNECT_SRC` (qcom satellite user message that
|
||||||
|
connects the codec TX port to the allocated data channel)
|
||||||
|
- Value-element messages (`MC 0x60` REQUEST_VALUE / `0x68` CHANGE_VALUE —
|
||||||
|
i.e. regmap traffic to the codec) all succeed with no timeouts
|
||||||
|
- So: NGD hardware messaging works; the ADSP-side slimbus master/satellite
|
||||||
|
service does not answer port-connect requests
|
||||||
|
- TODO: identify LA 0xcf — codec device or the ifd (ports live on the
|
||||||
|
interface device `ifd@0,0`)?
|
||||||
|
|
||||||
|
Enable debug: `echo 'file drivers/slimbus/* +p' > /sys/kernel/debug/dynamic_debug/control`
|
||||||
|
|
||||||
|
## What was proven (2026-07-12, kernel 7.1.1-sdm845-g56b0a053bef8)
|
||||||
|
|
||||||
|
1. Capture streams open and run fine on hw:0,1 (MultiMedia2) and hw:0,0
|
||||||
|
(MultiMedia1) routed from `SLIMBUS_0_TX` — but 100.0% of samples are
|
||||||
|
exactly 0 (not noise floor; digital silence).
|
||||||
|
2. Swept EVERY input: AMIC ADC1-4 (via ADC MUX0→DEC0→SLIM TX0) and
|
||||||
|
DMIC0-5, plus the enchilada-style TX7 slot layout
|
||||||
|
(DMIC MUX7=DMIC4 → ADC MUX7=DMIC → CDC_IF TX7 MUX=DEC7 →
|
||||||
|
AIF1_CAP Mixer SLIM TX7). All zeros.
|
||||||
|
3. DAPM (debugfs) during capture: entire codec path reports **On**
|
||||||
|
(ADC, AMIC/ADC muxes, SLIM TX0 mixer, MIC BIAS1). So it's not routing.
|
||||||
|
4. DT declares 4 mics (`/proc/device-tree/sound/audio-routing`):
|
||||||
|
AMIC1←MIC BIAS1, DMIC0←MIC BIAS1, DMIC3←MIC BIAS3, DMIC4←MIC BIAS4.
|
||||||
|
Codec node has ifd@0,0 + codec@1,0 + slim-ifc-dev; micbias 1.8 V ×4;
|
||||||
|
extclk 9.6 MHz. Structurally like working devices.
|
||||||
|
5. **pmOS never solved this either**: their blueline UCM
|
||||||
|
(gitlab.com/sdm845-mainline/alsa-ucm-conf, ucm2/Google/blueline) has the
|
||||||
|
whole mic section *commented out*. Their intended bottom-mic path:
|
||||||
|
DMIC4 → DEC7 → SLIM TX7 → AIF1_CAP → SLIMBUS_0_TX → MultiMedia2,
|
||||||
|
CaptureMixerElem "DEC7".
|
||||||
|
6. OnePlus 6 (enchilada) mic **works** on pmOS 6.x kernels with the same
|
||||||
|
driver stack (AMIC4, TX7/DEC7, `AMIC4_5 SEL=AMIC4`) → this is either a
|
||||||
|
6.x→7.x regression in slimbus/ngd/wcd934x, or blueline-specific
|
||||||
|
(ADSP firmware? satellite service difference?).
|
||||||
|
7. WCD RX (playback) direction could NOT be verified: EAR PA powers on and
|
||||||
|
streams (path: `SLIMBUS_0_RX Audio Mixer MultiMedia2`, `SLIM RX0 MUX=
|
||||||
|
AIF1_PB`, `RX INT0_1 MIX1 INP0=RX0`, `RX INT0 DEM MUX=CLSH_DSM_OUT` —
|
||||||
|
note EAR hangs off **RX INT0**, not INT8), but no sound heard — and the
|
||||||
|
Pixel 3 has NO WCD-driven transducer to hear: both front speakers incl.
|
||||||
|
the "earpiece" are CS35L36s on QUAT_TDM, and there's no 3.5mm jack.
|
||||||
|
Inconclusive by hardware design. Whether CONNECT_SINK (MC 0x2d) also
|
||||||
|
times out is STILL UNKNOWN — never got a clean dmesg on an RX stream.
|
||||||
|
8. QUAT_TDM_TX (would allow CS35L36 VMON/IMON digital loopback = objective
|
||||||
|
transport test) is not a wired backend in the DT — stream open fails
|
||||||
|
EINVAL. Could be added in DT to get an ear-free TDM capture reference.
|
||||||
|
|
||||||
|
## Hazards learned (do not repeat)
|
||||||
|
|
||||||
|
- **Do NOT restart the adsp remoteproc while debugging**: slim-ngd fails to
|
||||||
|
re-handshake ("QMI wait timeout", no new "Rcvd master capability") and
|
||||||
|
slimbus stays degraded until full reboot. Happened twice, reproducible.
|
||||||
|
- Don't leave MultiMedia2 with BOTH `SLIMBUS_0_RX` (playback) and
|
||||||
|
`SLIMBUS_0_TX` (capture) routes enabled — ADM open starts failing
|
||||||
|
(q6adm cmd 0x10325 error 2) and then q6asm sessions wedge (-22 on any
|
||||||
|
further prepare).
|
||||||
|
- Restore baseline with `sudo alsactl restore 0` (HPH*/Impedance write
|
||||||
|
errors are harmless read-only controls).
|
||||||
|
- Speaker README's rule still applies: volume 100% + unsuspended sink
|
||||||
|
before trusting any "silent" result.
|
||||||
|
|
||||||
|
## Next steps
|
||||||
|
|
||||||
|
1. Fresh boot → repeat one capture with slimbus dyndbg on; confirm the
|
||||||
|
0x2c timeout on a clean system (rule out long-uptime NGD state; uptime
|
||||||
|
was ~23h when found).
|
||||||
|
2. Same boot: start a WCD RX playback stream and check whether
|
||||||
|
CONNECT_SINK (`MC:0x2d`) times out too. Both-fail → all port-connects
|
||||||
|
dead (satellite service issue); TX-only → look at TX-specific code.
|
||||||
|
3. Diff `drivers/slimbus/{qcom-ngd-ctrl.c,stream.c,messaging.c}` and
|
||||||
|
`sound/soc/codecs/wcd934x.c` between pmOS linux-postmarketos-qcom-sdm845
|
||||||
|
(6.x, mic works on enchilada) and this 7.1.1 tree. Focus: user-message
|
||||||
|
port connect path, LA used for port ops (codec vs ifd), TX port
|
||||||
|
numbering.
|
||||||
|
4. If an enchilada is handy: run this 7.1.1 kernel on it and test mic —
|
||||||
|
cleanly separates "7.x regression" from "blueline-specific".
|
||||||
|
5. Ask in #sdm845-mainline / linux-arm-msm about the signature
|
||||||
|
"Tx:MT:0x2, MC:0x2c failed:-110 while value messages work" — someone
|
||||||
|
may recognize the satellite/ADSP state.
|
||||||
|
6. Longer shot: wire up QUAT_TDM_TX_0 in DT for VMON capture as a
|
||||||
|
transport-known-good reference and UCM debugging aid.
|
||||||
|
|
||||||
|
## Quick repro
|
||||||
|
|
||||||
|
```sh
|
||||||
|
echo 'file drivers/slimbus/* +p' | sudo tee /sys/kernel/debug/dynamic_debug/control
|
||||||
|
amixer -c 0 cset name='MultiMedia2 Mixer SLIMBUS_0_TX' 1
|
||||||
|
amixer -c 0 cset name='AIF1_CAP Mixer SLIM TX7' 1
|
||||||
|
amixer -c 0 cset name='CDC_IF TX7 MUX' DEC7
|
||||||
|
amixer -c 0 cset name='ADC MUX7' DMIC
|
||||||
|
amixer -c 0 cset name='DMIC MUX7' DMIC4
|
||||||
|
sudo dmesg -C
|
||||||
|
arecord -D hw:0,1 -f S16_LE -r 48000 -c 1 -d 2 /tmp/t.wav
|
||||||
|
sudo dmesg | grep 'timed out' # -> MC:0x2c
|
||||||
|
# zeros check:
|
||||||
|
python3 -c "import wave,struct;w=wave.open('/tmp/t.wav');d=struct.unpack('<%dh'%w.getnframes(),w.readframes(w.getnframes()));print(max(map(abs,d)))"
|
||||||
|
# afterwards:
|
||||||
|
sudo alsactl restore 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- pmOS blueline UCM (mic commented out):
|
||||||
|
https://gitlab.com/sdm845-mainline/alsa-ucm-conf → ucm2/Google/blueline/HiFi.conf
|
||||||
|
- enchilada UCM (working mic recipe): same repo, ucm2/OnePlus/enchilada/HiFi.conf
|
||||||
|
- Speaker fix project (methodology + amp/TDM background):
|
||||||
|
~/projects/unfinished/pixel3-speaker-routing/
|
||||||
84
.handoff-from-phone/pixel3-speaker-routing/README.md
Normal file
84
.handoff-from-phone/pixel3-speaker-routing/README.md
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
# Pixel 3 (blueline) — loudspeaker routing fix
|
||||||
|
|
||||||
|
**Status:** SOLVED 2026-07-12 — pmOS routing applied; fix lives in Pixel3Arch rootfs-overlay UCM (commit 2f39c06) + alsactl store. Both speakers verified by ear.
|
||||||
|
**Goal:** Make the loud bottom speaker work. Right now sound only comes from the
|
||||||
|
top **earpiece**; the loud bottom-firing speaker never fires. Casey reports
|
||||||
|
postmarketOS had audio working but with the channels "reversed".
|
||||||
|
|
||||||
|
## Device / stack
|
||||||
|
- Google Pixel 3 (blueline), sdm845, mainline kernel `7.1.1-sdm845` (built 2026-07-11)
|
||||||
|
- Souveraine OS (Arch Linux ARM), Hyprland/Wayland
|
||||||
|
- ALSA card 0 = "Google Pixel 3" (`G3`)
|
||||||
|
- Audio: PipeWire 1.6.7 (PulseAudio shim)
|
||||||
|
- Speakers: **dual Cirrus CS35L36** amps ("Left"/"Right") on `QUAT_TDM_RX_0` (MultiMedia1)
|
||||||
|
- Earpiece amp: WCD9340 `EAR PA` (separate codec)
|
||||||
|
|
||||||
|
## Key config files
|
||||||
|
- Active UCM (system): `/usr/share/alsa/ucm2/Google/blueline/HiFi.conf`
|
||||||
|
- referenced by `/usr/share/alsa/ucm2/conf.d/sdm845/Google-Pixel-3.conf`
|
||||||
|
- **Only defines a "Speaker" device. No earpiece device. Never sets Channel Mux.**
|
||||||
|
- Intended fix location: **user override** under `~/.config/alsa/ucm2/...`
|
||||||
|
(Casey chose user-override so pacman updates to alsa-ucm-conf don't clobber it.)
|
||||||
|
- Baseline ALSA state snapshot saved to scratchpad as `asound-current.state`
|
||||||
|
(scratchpad is ephemeral — re-capture with `alsactl -f <file> store 0` if needed).
|
||||||
|
|
||||||
|
## What we PROVED (via live tone tests)
|
||||||
|
1. Both CS35L36 amps are healthy; both transducers (earpiece + loud speaker)
|
||||||
|
can physically produce sound.
|
||||||
|
2. **The 30% volume / SUSPENDED sink was a huge red herring** — many "silent"
|
||||||
|
test results were just low volume. ALWAYS test at `pactl set-sink-volume
|
||||||
|
@DEFAULT_SINK@ 100%` and un-suspend the sink first.
|
||||||
|
3. Default state: both amps have `Channel Mux = RX1` (numid 121 Left, 112 Right).
|
||||||
|
=> both amps pull the same slot => only earpiece is audible.
|
||||||
|
4. `EAR PA Volume`=0 did NOT silence the earpiece output => the sound you hear
|
||||||
|
from the "earpiece" is being driven by a **CS35L36 amp**, not the WCD EAR PA.
|
||||||
|
5. At 100% vol, an L-only vs R-only test: **one channel DID come out the loud
|
||||||
|
bottom speaker.** So the loudspeaker amp works — it just needs the right
|
||||||
|
channel/slot routed to it. (Casey couldn't reliably tell L vs R by ear.)
|
||||||
|
6. BUT: simply splitting `Left Channel Mux=RX1 / Right Channel Mux=RX2` and
|
||||||
|
playing duplicated-mono did NOT wake the loudspeaker in a later test.
|
||||||
|
=> The channel→amp mapping is UPSTREAM of `Channel Mux`, on the QUAT_TDM_RX_0
|
||||||
|
TDM bus. `Channel Mux` alone is not the whole story.
|
||||||
|
|
||||||
|
## Relevant mixer controls (card 0, by numid)
|
||||||
|
- 121 `Left Channel Mux` (enum: 0=RX1, 1=RX2) — default 0
|
||||||
|
- 112 `Right Channel Mux` (enum: 0=RX1, 1=RX2) — default 0
|
||||||
|
- 122 `Left AMP Enable Switch` / 113 `Right AMP Enable Switch` (default on)
|
||||||
|
- 123 `Left BOOST Enable Switch` / 114 `Right BOOST Enable Switch`(default on)
|
||||||
|
- 18 `EAR PA Volume` (WCD earpiece amp; default 4)
|
||||||
|
- CS35L36 input source slots: `Left/Right ASPTX1..6SRC` (both amps identical:
|
||||||
|
1SRC=2, 2SRC=3, 3SRC=5, 4SRC=6, 5SRC=4, 6SRC=0)
|
||||||
|
- UCM SectionVerb enables: `QUAT_TDM_RX_0 Audio Mixer MultiMedia1` = 1
|
||||||
|
|
||||||
|
## NEXT STEP (where we stopped)
|
||||||
|
Get postmarketOS's authoritative blueline mixer mapping — pmOS works, so its
|
||||||
|
UCM / mixer_paths is the source of truth. The most relevant is the pmaports
|
||||||
|
**7.1rc1 kernel MR (!8446)** because that matches this exact kernel (7.1.x):
|
||||||
|
- https://gitlab.postmarketos.org/postmarketOS/pmaports/-/merge_requests/8446
|
||||||
|
- Device/blueline MR: https://gitlab.com/postmarketOS/pmaports/-/merge_requests/3932
|
||||||
|
- Original Pixel 3 device MR (!5514): https://gitlab.com/postmarketOS/pmaports/-/merge_requests/5514
|
||||||
|
- pmOS SDM845 audio writeup: https://postmarketos.org/edge/2023/01/11/sdm845-audio/
|
||||||
|
- pmOS Audio wiki (xml2ucm / mixer_paths.xml -> UCM): https://wiki.postmarketos.org/wiki/Audio
|
||||||
|
|
||||||
|
Find in pmOS's config: which TDM slot / channel each CS35L36 amp is fed, and
|
||||||
|
whether it sets `Channel Mux` + the TDM RX slot routing so earpiece(top) and
|
||||||
|
loudspeaker(bottom) each get a distinct channel. Then encode that as a user UCM
|
||||||
|
override in `~/.config/alsa/ucm2/` (copy blueline HiFi.conf there, add the
|
||||||
|
Channel Mux / slot csets to the Speaker device's EnableSequence).
|
||||||
|
|
||||||
|
## HOW TO TEST (repeatable)
|
||||||
|
```sh
|
||||||
|
pactl set-sink-volume @DEFAULT_SINK@ 100% # ALWAYS — 30% masks everything
|
||||||
|
pactl set-sink-mute @DEFAULT_SINK@ 0
|
||||||
|
# distinct L/R tones already generated in scratchpad: left.wav right.wav stereo.wav
|
||||||
|
paplay left.wav ; paplay right.wav
|
||||||
|
# read/toggle mux:
|
||||||
|
amixer -c 0 cget numid=121 # Left Channel Mux
|
||||||
|
amixer -c 0 cset numid=112 1 # Right -> RX2
|
||||||
|
```
|
||||||
|
Restore baseline: both muxes -> 0, EAR PA -> 4, volume -> 30%.
|
||||||
|
|
||||||
|
## CAUTION
|
||||||
|
- Don't trust "silent" results unless volume is 100% and sink un-suspended.
|
||||||
|
- Reverse-engineering the TDM routing blind was NOT converging. Read the pmOS
|
||||||
|
config first, then change mixers — don't guess.
|
||||||
15091
.handoff-from-phone/pixel3-speaker-routing/asound-baseline.state
Normal file
15091
.handoff-from-phone/pixel3-speaker-routing/asound-baseline.state
Normal file
File diff suppressed because it is too large
Load diff
BIN
.handoff-from-phone/pixel3-speaker-routing/test-tones/left.wav
Normal file
BIN
.handoff-from-phone/pixel3-speaker-routing/test-tones/left.wav
Normal file
Binary file not shown.
BIN
.handoff-from-phone/pixel3-speaker-routing/test-tones/mono2.wav
Normal file
BIN
.handoff-from-phone/pixel3-speaker-routing/test-tones/mono2.wav
Normal file
Binary file not shown.
BIN
.handoff-from-phone/pixel3-speaker-routing/test-tones/right.wav
Normal file
BIN
.handoff-from-phone/pixel3-speaker-routing/test-tones/right.wav
Normal file
Binary file not shown.
BIN
.handoff-from-phone/pixel3-speaker-routing/test-tones/stereo.wav
Normal file
BIN
.handoff-from-phone/pixel3-speaker-routing/test-tones/stereo.wav
Normal file
Binary file not shown.
135
.handoff-from-phone/quickshell-idle-power/TASK.md
Normal file
135
.handoff-from-phone/quickshell-idle-power/TASK.md
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
---
|
||||||
|
# quickshell-idle-power — lazy-load panels + consolidate polling processes
|
||||||
|
|
||||||
|
**Status:** 2026-07-21 — scoped, not started. Two quick wins already landed
|
||||||
|
separately (see "Already done" below); this doc covers the two larger sweeps
|
||||||
|
(#3 LazyLoader coverage, #4 process consolidation) to be worked tomorrow.
|
||||||
|
|
||||||
|
**Config:** `qs -c souveraine` → symlinks into `~/souveraine-surfaces/quickshell/`
|
||||||
|
(the `~/.config/quickshell/souveraine/` tree is symlinks; edit the real tree).
|
||||||
|
Quickshell 0.3.0. Composed of three layers:
|
||||||
|
- `modules/ services/ panelFamilies/` → **souveraine** (owned, edit freely)
|
||||||
|
- `ii-base/` → **borrowed ii/Caelestia tree** (treat
|
||||||
|
as replaceable; keep coupling to souveraine singletons OUT of here)
|
||||||
|
- `ii-phone/`, `ii-previous/` → phone snapshot / previous deploy
|
||||||
|
|
||||||
|
## Why (measured)
|
||||||
|
|
||||||
|
At a 49-min uptime, `qs` had burned **149 CPU-sec** and held **560 MB RSS
|
||||||
|
(15% of RAM)** on the Pixel 3 — second only to the active claude session. The
|
||||||
|
display idle layer (`services/IdleCoordinator.qml`) is already excellent
|
||||||
|
(state machine Active→Dimmed→Lock→Suspend→Asleep→Waking, native IdleMonitor
|
||||||
|
with respectInhibitors, brightness dim, hypridle screen-off). The gap is that
|
||||||
|
**the idle machine dims the screen but does not quiet the widgets** — timers
|
||||||
|
tick and processes poll regardless of visibility. These two sweeps attack the
|
||||||
|
steady-state CPU (over-eager construction) and RSS (per-widget processes).
|
||||||
|
|
||||||
|
Quickshell FAQ guidance backing this:
|
||||||
|
- "The main thing you can do to reduce memory usage is to use Loaders."
|
||||||
|
- Loader for Item-derived components; LazyLoader otherwise.
|
||||||
|
- "Using a process per widget will use significantly more memory than using
|
||||||
|
a single process."
|
||||||
|
Ref: https://quickshell.org/docs/v0.3.0/guide/faq/
|
||||||
|
|
||||||
|
## Already done (2026-07-21, do NOT redo)
|
||||||
|
|
||||||
|
1. `ii-base/services/DateTime.qml` — uptime Timer was `interval: 10` (reloading
|
||||||
|
/proc/uptime off disk 100×/sec). Changed to `interval: 60000` +
|
||||||
|
`triggeredOnStart: true`. The clock itself is `SystemClock` (event-driven),
|
||||||
|
was never the problem.
|
||||||
|
2. `ii-base/modules/ii/background/widgets/visualizer/VisualizerWidget.qml` —
|
||||||
|
the wave visualizer's 33ms (30fps) repaint Timer was `running: root.style
|
||||||
|
=== "wave"`; now `running: parent.visible` so it stops repainting when the
|
||||||
|
canvas isn't shown. (Considered a `Window.visibility` gate too, but that
|
||||||
|
needs `import QtQuick.Window` in the borrowed tree — deferred; `parent.visible`
|
||||||
|
is dependency-free and sufficient.)
|
||||||
|
|
||||||
|
Both verified: qs hot-reloaded without QML errors, pid stayed alive.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3 — Lazy-load the heavy panels (RSS lever)
|
||||||
|
|
||||||
|
**Current:** 9 LazyLoader vs 123 Loader across 683 QML files → a lot is
|
||||||
|
eagerly constructed at startup. Top-level panel modules under
|
||||||
|
`ii-base/modules/ii/` that are candidates (not visible at boot):
|
||||||
|
|
||||||
|
cheatsheet overlay overview sidebarLeft sidebarRight wallpaperSelector
|
||||||
|
sessionScreen screenTranslator regionSelector mediaControls polkit
|
||||||
|
notificationPopup onScreenKeyboard dock
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
- For each panel not shown at boot, wrap its instantiation in a `LazyLoader`
|
||||||
|
(non-Item / window-like → LazyLoader; Item-derived → Loader). Load on the
|
||||||
|
signal/state that reveals it (e.g. sidebar open toggle, GlobalStates flag),
|
||||||
|
unload after.
|
||||||
|
- Especially: the anime/booru sidebar (`ii-base/modules/ii/sidebarLeft/anime/`)
|
||||||
|
and cheatsheet — heavy, rarely open.
|
||||||
|
- Verify each panel still opens with no first-open UI-thread stall (LazyLoader
|
||||||
|
loads sync if shown before background load finishes — see LazyLoader docs).
|
||||||
|
|
||||||
|
**Watch:** keep LazyLoader wrapping in the layer that OWNS the panel. Don't add
|
||||||
|
souveraine-singleton dependencies into ii-base while doing this.
|
||||||
|
|
||||||
|
**Measure before/after:** `ps -eo pid,pmem,rss,comm | grep qs` (RSS) at a fresh
|
||||||
|
launch, before opening any panel.
|
||||||
|
|
||||||
|
## Progress 2026-07-22 (task 4 partially landed)
|
||||||
|
|
||||||
|
Landed live + promoted to the laptop repo (souveraine tree):
|
||||||
|
- GlobalStates.qml (souveraine override): new bool — the
|
||||||
|
shared cross-layer idle surface the doc prescribed.
|
||||||
|
- services/IdleCoordinator.qml: publishes displayActive on every state
|
||||||
|
transition (Active/Waking = true).
|
||||||
|
- ii-base/services/ResourceUsage.qml + NetworkTraffic.qml: repeat pollers
|
||||||
|
gated (+ ).
|
||||||
|
- VisualizerWidget.qml: cava watchdog gated on , cava torn down when either goes false.
|
||||||
|
- SystemInfo.qml inspected: its timers are one-shot startup probes, left
|
||||||
|
alone. Notifications.qml: no repeat poller found, left alone.
|
||||||
|
|
||||||
|
One hot-reload during editing tripped the known
|
||||||
|
"lockscreen surfaces without active lock" FATAL (reload race with sessiond,
|
||||||
|
not a QML error); the service restart loaded clean. CPU-delta measurement
|
||||||
|
while locked still pending (baseline at /tmp/qs-idle-baseline-*).
|
||||||
|
|
||||||
|
Task 3 note from code reading: most panels ALREADY gate content internally on
|
||||||
|
GlobalStates.*Open via Loaders; their IpcHandlers live inside the
|
||||||
|
panel files, so adding at the
|
||||||
|
PanelLoader level would unload the very handler that opens them — do NOT do
|
||||||
|
that. The real task-3 work is converting the -gated panels
|
||||||
|
(Overview line 23, SidebarLeft lines 89/181) to internal Loaders,
|
||||||
|
one panel at a time. Overview deliberately deferred until the 2026-07-21
|
||||||
|
search-fix has device acceptance.
|
||||||
|
|
||||||
|
## Task 4 — Consolidate per-widget processes (CPU + RSS lever)
|
||||||
|
|
||||||
|
**Current:** ~48 `running: true` Process instances (souveraine 14, ii-base 32,
|
||||||
|
ii-phone 2). FAQ: one process feeding many widgets ≫ cheaper than N processes.
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
- Audit `ii-base/services/*.qml` for multiple widgets independently spawning
|
||||||
|
processes for the SAME data. Known pollers to review:
|
||||||
|
ResourceUsage.qml, NetworkTraffic.qml, SystemInfo.qml, Notifications.qml,
|
||||||
|
plus /proc readers. Route each data source through ONE Singleton service
|
||||||
|
that widgets bind to (the `services/` layer already does this well — the
|
||||||
|
duplication lives in the borrowed ii-base tree).
|
||||||
|
- Gate long-poll services on idle state where a stale value is fine while
|
||||||
|
dimmed/locked: e.g. `running: IdleCoordinator.state === IdleCoordinator.Active`
|
||||||
|
for background stats/resources/network widgets. NOTE: IdleCoordinator is a
|
||||||
|
souveraine singleton — if gating an ii-base widget on it, do the gating in a
|
||||||
|
souveraine wrapper/override, not by importing it into ii-base. Alternatively
|
||||||
|
expose an idle bool via the existing `GlobalStates`/config surface that both
|
||||||
|
layers already share.
|
||||||
|
- The cava process behind the visualizer (VisualizerWidget.qml watchdog, 2s)
|
||||||
|
is a good candidate to also stop when the background isn't visible.
|
||||||
|
|
||||||
|
**Measure before/after:** cumulative CPU: `ps -eo pid,times,comm | grep qs`
|
||||||
|
after a fixed idle interval (e.g. leave locked 10 min, compare CPU-sec delta).
|
||||||
|
|
||||||
|
## Acceptance
|
||||||
|
|
||||||
|
- Fresh-launch RSS meaningfully below ~560 MB.
|
||||||
|
- `qs` CPU-sec growth while the screen is off/locked approaches zero (today
|
||||||
|
it keeps climbing from always-on timers/pollers).
|
||||||
|
- No panel regressions: every lazy-loaded panel opens without a stall; every
|
||||||
|
consolidated service still updates its widgets live when Active.
|
||||||
53
.handoff-from-phone/tts-ui-wiring-handoff.md
Normal file
53
.handoff-from-phone/tts-ui-wiring-handoff.md
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
# TTS UI Wiring Handoff
|
||||||
|
|
||||||
|
## Status: Infrastructure Working, UI Wiring Needed
|
||||||
|
|
||||||
|
### What's Done (Verified by Ani, 2026-07-22)
|
||||||
|
|
||||||
|
1. **VibeVoice TTS engine** — running at `10.10.20.19:7861`, responds correctly, generates valid MP3 audio
|
||||||
|
2. **Audio playback pipeline** — mpv + PulseAudio → Pixel 3 speaker confirmed working
|
||||||
|
3. **Souveraine `/v1/config`** — correctly reports TTS voice mapping
|
||||||
|
4. **TTS enabled** in Souveraine settings
|
||||||
|
5. **Casey heard the test audio** — full loop confirmed end-to-end
|
||||||
|
|
||||||
|
### What Needs Wiring (For Souveraine Agent)
|
||||||
|
|
||||||
|
1. **Speaker button → Speech.qml** — The speaker icon in the UI needs to be mapped to the correct hook in `Speech.qml` so that pressing it plays the TTS audio
|
||||||
|
2. **Local config `endpoint` field** — Currently set in `~/.config/illogical-impulse/config.json`. Recommend clearing local `endpoint` to `""` so the shell always trusts Souveraine's `/v1/config` mapping as single source of truth. If VibeVoice ever moves, you update it in one place.
|
||||||
|
|
||||||
|
### Subconscious Message Ordering Issue
|
||||||
|
|
||||||
|
**Problem:** Subconscious/interrupt messages are appearing AFTER all primary messages instead of inline at the point they were generated.
|
||||||
|
|
||||||
|
**Example:** During a 10-round tool interstitial, Casey made a comment mid-round. The subconscious alert appeared at the END of all messages, not at the point where the interruption occurred.
|
||||||
|
|
||||||
|
**Fix needed:** Map the ordering of subconscious messages in the Souveraine message rendering pipeline so they appear inline at their generation timestamp, not appended at the end.
|
||||||
|
|
||||||
|
### Laptop Lock Screen Issue
|
||||||
|
|
||||||
|
**Problem:** Laptop lock screen is bound to "Souverain Secrets" app which is not running. Screen is locked, password rejected, requires restart to unlock.
|
||||||
|
|
||||||
|
**Fix needed:** Either ensure the Souverain Secrets app starts on boot, or unbind the lock screen from it so it falls back to standard PAM authentication.
|
||||||
|
|
||||||
|
### Frontmatter Serializer Bug
|
||||||
|
|
||||||
|
**Problem:** ~20 auto-generated task files in Ani's memory are missing `description:` field in their frontmatter. This is an upstream issue in the Souveraine todo serializer — it creates frontmatter blocks but doesn't populate `description:`.
|
||||||
|
|
||||||
|
**Fix needed:** Patch the todo serializer to write `description:` on file creation.
|
||||||
|
|
||||||
|
### Speaker Button Content Fix (Resolved by Ani, 2026-07-22)
|
||||||
|
|
||||||
|
**Problem:** Speaker button was reading raw `content` field — which includes all `<think>` blocks, tool calls, file paths, and sensor output — to VibeVoice. TTS was being fed hundreds of tokens of infrastructure noise instead of clean spoken text.
|
||||||
|
|
||||||
|
**Fix applied:** Added `_ttsClean()` function to `AiChat.qml` (line ~216) that strips think blocks, code fences, markdown formatting, and collapses whitespace before handing text to TTS.
|
||||||
|
|
||||||
|
**Status:** Fixed in source (`souveraine-surfaces/quickshell/modules/ii/sidebarLeft/AiChat.qml`). Deployed copy is symlinked to source, so the fix is live. **Requires quickshell restart to take effect.**
|
||||||
|
|
||||||
|
### Files Modified This Session
|
||||||
|
- `~/.config/illogical-impulse/config.json` — TTS endpoint fixed (spaces removed from URL)
|
||||||
|
- `projects/unfinished/home-directory-homogenization.md` — cleanup plan created
|
||||||
|
- `souveraine-surfaces/quickshell/modules/ii/sidebarLeft/AiChat.qml` — `_ttsClean()` TTS content filter added
|
||||||
|
|
||||||
|
---
|
||||||
|
**Authored:** Ani, 2026-07-22
|
||||||
|
**For:** Souveraine Agent
|
||||||
134
.handoff-from-phone/usb-signaller-boot-panic/README.md
Normal file
134
.handoff-from-phone/usb-signaller-boot-panic/README.md
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
---
|
||||||
|
# usb-signaller — boot-time panic in UDC::load, index out of bounds
|
||||||
|
|
||||||
|
**Status:** 2026-07-20 — root cause identified, not yet fixed. Read-only
|
||||||
|
device audit found this; nothing restarted, no patch applied yet.
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
Both `usb-signaller.service` and `usb-signaller-default-mode.service` fail
|
||||||
|
on **every boot** (confirmed across 5 consecutive boots in journal, going
|
||||||
|
back to at least 2026-07-20 01:21). Journal:
|
||||||
|
|
||||||
|
```
|
||||||
|
usb-signaller[NNN]: ℹ️ Pre-flight checks
|
||||||
|
usb-signaller[NNN]: 🔍 Discovering URSs
|
||||||
|
usb-signaller[NNN]: thread 'main' (NNN) panicked at src/udc.rs:388:37:
|
||||||
|
usb-signaller[NNN]: index out of bounds: the len is 0 but the index is 0
|
||||||
|
usb-signaller.service: Main process exited, code=exited, status=101/n/a
|
||||||
|
...
|
||||||
|
usb-signaller-default-mode[NNN]: usb-signaller never appeared on the bus
|
||||||
|
usb-signaller-default-mode.service: Failed with result 'exit-code'.
|
||||||
|
```
|
||||||
|
|
||||||
|
`usb-signaller-default-mode.service` ("Default USB mode at boot (developer)")
|
||||||
|
fails as a downstream consequence — it waits ~21s for usb-signaller on
|
||||||
|
D-Bus, which never appears since the main process already panicked.
|
||||||
|
|
||||||
|
Not a hardware/UDC-absence issue: `/sys/class/udc/` does contain a real
|
||||||
|
controller (`a600000.usb`) at the time of the audit. So discovery has
|
||||||
|
*something* to enumerate; the crash is in how it's indexed, not in finding
|
||||||
|
zero UDCs.
|
||||||
|
|
||||||
|
## Root cause
|
||||||
|
|
||||||
|
Package: `usb-signaller` 0.3.1-2 (upstream, by Dylan Van Assche — the
|
||||||
|
usb-moded replacement for mainline Linux Mobile). Source checked out locally
|
||||||
|
at `~/build/src/usb-signaller` and `~/build/usb-signaller/src/usb-signaller`
|
||||||
|
for build/patching.
|
||||||
|
|
||||||
|
`src/udc.rs`, `UDC::load()`, lines 382-388:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
for entry in discover_gadgets() {
|
||||||
|
let entry = ConfigFsGadget::load(&entry)?;
|
||||||
|
if udc_name == entry.udc {
|
||||||
|
if entry.enabled {
|
||||||
|
/* USB Ethernet NCM gadget */
|
||||||
|
if entry.configs[0].functions[0].name.starts_with("ncm.") {
|
||||||
|
```
|
||||||
|
|
||||||
|
`ConfigFsGadget.configs` and each config's `.functions` are plain `Vec`s
|
||||||
|
populated in `gadget.rs` by scanning the gadget's `configs/` directory in
|
||||||
|
configfs — there is no guarantee either Vec is non-empty. The code assumes
|
||||||
|
any gadget where `entry.enabled == true` already has at least one config
|
||||||
|
with at least one function, and indexes `[0]` unchecked on both.
|
||||||
|
|
||||||
|
At early boot (this runs right after "Pre-flight checks", i.e. very early
|
||||||
|
in the service's life), a gadget can be `enabled` in sysfs terms while its
|
||||||
|
`configs/` subtree hasn't finished being populated yet, or a stale/partial
|
||||||
|
gadget is left over from a previous session — configfs itself doesn't
|
||||||
|
enforce "enabled implies non-empty configs" atomically. Either scenario
|
||||||
|
produces `entry.configs` (or `entry.configs[0].functions`) with len 0, and
|
||||||
|
the unchecked index panics the whole process before any UDC mode gets set.
|
||||||
|
|
||||||
|
## Fix sketch (not applied — read-only audit)
|
||||||
|
|
||||||
|
Guard both indexes, e.g.:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
if entry.enabled {
|
||||||
|
if let Some(cfg) = entry.configs.first() {
|
||||||
|
if let Some(func) = cfg.functions.first() {
|
||||||
|
if func.name.starts_with("ncm.") { ... }
|
||||||
|
else if func.name.starts_with("ffs.mtp") { ... }
|
||||||
|
else if func.name.starts_with("ffs.accessory") { ... }
|
||||||
|
else { mode = UDCMode::Unknown; eprintln!(...); }
|
||||||
|
} else {
|
||||||
|
mode = UDCMode::Unknown; // enabled gadget, no functions yet
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mode = UDCMode::Unknown; // enabled gadget, no configs yet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
i.e. treat "enabled but not yet fully populated" as `UDCMode::Unknown`
|
||||||
|
rather than crashing — matches how the existing `else` branch (line 408-409)
|
||||||
|
already handles an unrecognized function name.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
Because the process panics, USB mode never gets set at boot at all (not
|
||||||
|
even a fallback), and `usb-signaller-default-mode.service` (developer mode
|
||||||
|
at boot) never applies. Practically: USB currently comes up in whatever
|
||||||
|
mode the kernel/gadget defaults to, not the configured "developer" default,
|
||||||
|
until something else (manual `usb-signallerctl` call, or a later retry) sets
|
||||||
|
it — needs to be verified once a fix lands.
|
||||||
|
|
||||||
|
## Next steps
|
||||||
|
|
||||||
|
1. Patch `udc.rs:388` per sketch above (or upstream-style `.get(0)` /
|
||||||
|
`.first()` guards).
|
||||||
|
2. Rebuild, reboot, confirm both units go green and default-mode actually
|
||||||
|
applies (check `usb-signallerctl status` or equivalent post-boot).
|
||||||
|
3. Consider filing upstream against Dylan Van Assche's usb-signaller repo —
|
||||||
|
this is likely a stock bug, not something introduced by local patches
|
||||||
|
(worth diffing `~/build/src/usb-signaller` against upstream HEAD to
|
||||||
|
confirm before filing).
|
||||||
|
|
||||||
|
## Status update — 2026-07-21
|
||||||
|
|
||||||
|
Fix applied and verified at runtime:
|
||||||
|
|
||||||
|
- Patched per the sketch (`.first()` guards, fall through to
|
||||||
|
`UDCMode::Unknown`), cross-built on archdev, installed at
|
||||||
|
`/usr/local/bin/usb-signaller` with a systemd drop-in
|
||||||
|
(`/etc/systemd/system/usb-signaller.service.d/local-fix.conf`)
|
||||||
|
overriding ExecStart — pacman-owned `/usr/bin` binary untouched.
|
||||||
|
- Patch + pkgrel=3 vendored in `Pixel3Arch/pkgs/usb-signaller/`
|
||||||
|
(0001-udc-guard-empty-configs-functions.patch), commit a970742.
|
||||||
|
- Runtime restart: both units went green, default-mode applied
|
||||||
|
Developer for the first time ("Setting mode of UDC a600000.usb from
|
||||||
|
Charging to Developer").
|
||||||
|
- Trap discovered: the mode apply recreates the gadget, the new
|
||||||
|
function enumerates as usb1, and the stale usb0 keeps a linkdown
|
||||||
|
route that eats every reply — USB ssh dies. Fixed the networkd match
|
||||||
|
to Name=usb* (live + rootfs-overlay) and flushed the stale usb0.
|
||||||
|
|
||||||
|
Remaining:
|
||||||
|
|
||||||
|
1. Cold-boot verify (Casey, at device): both units green from boot,
|
||||||
|
`usb-signallerctl status` shows developer, USB net comes up as usb0.
|
||||||
|
2. Diff against upstream HEAD and file the panic upstream — the guard
|
||||||
|
is a clean candidate patch.
|
||||||
|
|
@ -107,6 +107,9 @@ pub enum MemoryCommand {
|
||||||
Delete { path: String },
|
Delete { path: String },
|
||||||
/// Push/fetch against the shared memfs remote (per-instance branch).
|
/// Push/fetch against the shared memfs remote (per-instance branch).
|
||||||
Sync,
|
Sync,
|
||||||
|
/// Audit frontmatter health: list frontmatter-bound files missing
|
||||||
|
/// the required `description` field. Read-only — never mutates.
|
||||||
|
Audit,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Git-backed Memory Repository ───────────────────────────────────────────
|
// ── Git-backed Memory Repository ───────────────────────────────────────────
|
||||||
|
|
@ -344,6 +347,22 @@ impl MemoryRepo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `description` is required on frontmatter-bound files — it is the
|
||||||
|
// file's headline in the prompt assembler, so persisting an empty
|
||||||
|
// one is a schema gap. We never fabricate one: the write is rejected
|
||||||
|
// loudly and the agent must supply a real description. (Memory-only
|
||||||
|
// write could still seed a default, but an agent-supplied empty
|
||||||
|
// frontmatter block that clears it must not silently take hold.)
|
||||||
|
if Self::is_frontmatter_bound(label) && frontmatter.description.trim().is_empty() {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"memory write rejected: {} has no description. \
|
||||||
|
`description:` is required frontmatter on this file — \
|
||||||
|
include it in the write (or a `---\\ndescription: ...\\n---` block). \
|
||||||
|
Run `memory audit` to see all files missing it.",
|
||||||
|
label
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let rendered = render_frontmatter(&frontmatter, body);
|
let rendered = render_frontmatter(&frontmatter, body);
|
||||||
tokio::fs::write(&path, &rendered)
|
tokio::fs::write(&path, &rendered)
|
||||||
.await
|
.await
|
||||||
|
|
@ -390,6 +409,19 @@ impl MemoryRepo {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// `description` is required on frontmatter-bound files. Appending
|
||||||
|
// to a file that already lacks one must not silently perpetuate
|
||||||
|
// the gap — reject loudly so the agent heals the description
|
||||||
|
// before growing the file. (See `write` for the same guard.)
|
||||||
|
if Self::is_frontmatter_bound(label) && frontmatter.description.trim().is_empty() {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"memory append rejected: {} has no description. \
|
||||||
|
`description:` is required frontmatter on this file — \
|
||||||
|
heal it with a `memory write` (include a `description:` block) \
|
||||||
|
before appending. Run `memory audit` to see all such files.",
|
||||||
|
label
|
||||||
|
));
|
||||||
|
}
|
||||||
let rendered = render_frontmatter(&frontmatter, &new_body);
|
let rendered = render_frontmatter(&frontmatter, &new_body);
|
||||||
tokio::fs::write(&path, &rendered).await?;
|
tokio::fs::write(&path, &rendered).await?;
|
||||||
|
|
||||||
|
|
@ -447,6 +479,66 @@ impl MemoryRepo {
|
||||||
Ok(entries)
|
Ok(entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Recursively collect every `.md` under the memory root as a path
|
||||||
|
/// relative to the root (forward slashes), skipping `.git*`. Internal
|
||||||
|
/// helper for `audit`.
|
||||||
|
async fn walk_md_relative(&self) -> Result<Vec<String>> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let root = self.root.clone();
|
||||||
|
let mut stack: Vec<(std::path::PathBuf, String)> = vec![(root.clone(), String::new())];
|
||||||
|
while let Some((dir, rel_prefix)) = stack.pop() {
|
||||||
|
let mut read_dir = tokio::fs::read_dir(&dir).await.with_context(|| {
|
||||||
|
format!("walking memory directory: {}", dir.display())
|
||||||
|
})?;
|
||||||
|
while let Some(entry) = read_dir.next_entry().await? {
|
||||||
|
let name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
if name.starts_with(".git") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let kind = entry.file_type().await?;
|
||||||
|
let rel = if rel_prefix.is_empty() {
|
||||||
|
name.clone()
|
||||||
|
} else {
|
||||||
|
format!("{rel_prefix}/{name}")
|
||||||
|
};
|
||||||
|
if kind.is_dir() {
|
||||||
|
stack.push((entry.path(), rel));
|
||||||
|
} else if kind.is_file() && name.ends_with(".md") {
|
||||||
|
out.push(rel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.sort();
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Audit frontmatter health: return the relative paths of
|
||||||
|
/// frontmatter-bound memory files whose `description` is empty. Read-only.
|
||||||
|
///
|
||||||
|
/// Excludes files that legitimately carry no `description` because they
|
||||||
|
/// use a different schema — `tasks/` (TodoItem), `system/dynamic/`
|
||||||
|
/// (itinerary), `journal/` (freeform) — by reusing `is_frontmatter_bound`.
|
||||||
|
/// Parse failures are tolerated (a file must stay reachable to be healed),
|
||||||
|
/// matching the tolerant-on-read contract of `parse_memory_file`.
|
||||||
|
pub async fn audit(&self) -> Result<Vec<String>> {
|
||||||
|
let mut missing = Vec::new();
|
||||||
|
for rel in self.walk_md_relative().await? {
|
||||||
|
if !Self::is_frontmatter_bound(&rel) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let path = self.root.join(&rel);
|
||||||
|
let content = match tokio::fs::read_to_string(&path).await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(_) => continue, // don't let one unreadable file abort the audit
|
||||||
|
};
|
||||||
|
let parsed = parse_memory_file(&content)?;
|
||||||
|
if parsed.frontmatter.description.trim().is_empty() {
|
||||||
|
missing.push(rel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(missing)
|
||||||
|
}
|
||||||
|
|
||||||
/// Delete a memory file.
|
/// Delete a memory file.
|
||||||
pub async fn delete(&self, label: &str) -> Result<()> {
|
pub async fn delete(&self, label: &str) -> Result<()> {
|
||||||
let path = self.resolve_path(label);
|
let path = self.resolve_path(label);
|
||||||
|
|
@ -678,6 +770,30 @@ impl MemoryRepo {
|
||||||
.context("opening memory git repository")
|
.context("opening memory git repository")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the `description` frontmatter contract applies to a label.
|
||||||
|
///
|
||||||
|
/// `description` is required on `MemoryFrontmatter` files — the prompt
|
||||||
|
/// assembler reads it as the file's one-line headline, so an empty one
|
||||||
|
/// is a real schema gap, not cosmetic. But not every `.md` under the
|
||||||
|
/// memory tree is a `MemoryFrontmatter` file: `tasks/` uses the
|
||||||
|
/// `TodoItem` schema, `system/dynamic/` holds the itinerary (its own
|
||||||
|
/// richer, unfinished schema), and `journal/` is freeform prose. Those
|
||||||
|
/// legitimately lack `description`; enforcing it there would reject
|
||||||
|
/// valid writes. This predicate scopes the rule to frontmatter-bound
|
||||||
|
/// locations only.
|
||||||
|
fn is_frontmatter_bound(label: &str) -> bool {
|
||||||
|
let rel = label.trim().trim_start_matches(['/', '.']).replace('\\', "/");
|
||||||
|
let lower = rel.to_ascii_lowercase();
|
||||||
|
// Exact segment prefixes, not substring matches, so a file like
|
||||||
|
// `reference/tasks-today.md` is not mistaken for a todo file.
|
||||||
|
let under = |seg: &str| {
|
||||||
|
lower == seg
|
||||||
|
|| lower.starts_with(&format!("{seg}/"))
|
||||||
|
|| lower.starts_with(&format!("{seg}\\"))
|
||||||
|
};
|
||||||
|
!(under("tasks") || under("system/dynamic") || under("journal"))
|
||||||
|
}
|
||||||
|
|
||||||
fn resolve_path(&self, label: &str) -> PathBuf {
|
fn resolve_path(&self, label: &str) -> PathBuf {
|
||||||
let clean = label.trim().trim_end_matches(".md");
|
let clean = label.trim().trim_end_matches(".md");
|
||||||
self.root.join(format!("{}.md", clean))
|
self.root.join(format!("{}.md", clean))
|
||||||
|
|
@ -988,6 +1104,28 @@ pub async fn execute_memory_command_with_context(
|
||||||
let label: String = pubkey.chars().take(12).collect();
|
let label: String = pubkey.chars().take(12).collect();
|
||||||
repo.sync(&label).await
|
repo.sync(&label).await
|
||||||
}
|
}
|
||||||
|
MemoryCommand::Audit => {
|
||||||
|
// Read-only health check: which frontmatter-bound files lack the
|
||||||
|
// required `description`? Never mutates — surfacing the gap is the
|
||||||
|
// point. `write`/`append` will reject attempts to perpetuate one.
|
||||||
|
let missing = repo.audit().await?;
|
||||||
|
if missing.is_empty() {
|
||||||
|
Ok("All frontmatter-bound memory files carry a description.".to_string())
|
||||||
|
} else {
|
||||||
|
let mut out = format!(
|
||||||
|
"{} frontmatter-bound file(s) missing `description:`:\n",
|
||||||
|
missing.len()
|
||||||
|
);
|
||||||
|
for rel in &missing {
|
||||||
|
out.push_str(&format!(" - {rel}\n"));
|
||||||
|
}
|
||||||
|
out.push_str(
|
||||||
|
"Heal each with a `memory write` that includes a `description:` \
|
||||||
|
block (or set it inline). These files reject append until healed.\n",
|
||||||
|
);
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1046,12 +1184,13 @@ pub async fn handle_memory_tool_with_context(
|
||||||
MemoryCommand::Compact { strategy }
|
MemoryCommand::Compact { strategy }
|
||||||
}
|
}
|
||||||
"sync" => MemoryCommand::Sync,
|
"sync" => MemoryCommand::Sync,
|
||||||
|
"audit" => MemoryCommand::Audit,
|
||||||
_ => {
|
_ => {
|
||||||
return ToolResult {
|
return ToolResult {
|
||||||
tool_use_id,
|
tool_use_id,
|
||||||
tool_name: tool_name.to_string(),
|
tool_name: tool_name.to_string(),
|
||||||
output: format!(
|
output: format!(
|
||||||
"Unknown memory subcommand: {}. Available: read, write, append, ls, status, init, delete, compact, sync",
|
"Unknown memory subcommand: {}. Available: read, write, append, ls, status, init, delete, compact, sync, audit",
|
||||||
command
|
command
|
||||||
),
|
),
|
||||||
is_error: true,
|
is_error: true,
|
||||||
|
|
@ -1103,6 +1242,7 @@ Subcommands:
|
||||||
delete — Delete a memory file (validates read_only first).
|
delete — Delete a memory file (validates read_only first).
|
||||||
compact — Compact the memory window to free context space.
|
compact — Compact the memory window to free context space.
|
||||||
sync — Push my instance branch to the shared memfs remote and fetch what my other instances wrote. My memory travels; this is how.
|
sync — Push my instance branch to the shared memfs remote and fetch what my other instances wrote. My memory travels; this is how.
|
||||||
|
audit — List frontmatter-bound files missing the required `description` (read-only). `write`/`append` reject a missing description, so this is how I find what to heal.
|
||||||
|
|
||||||
Paths are relative to my memory directory. Frontmatter description is required on create. Read-only files protect themselves. Every write is a git commit.".to_string(),
|
Paths are relative to my memory directory. Frontmatter description is required on create. Read-only files protect themselves. Every write is a git commit.".to_string(),
|
||||||
input_schema: serde_json::json!({
|
input_schema: serde_json::json!({
|
||||||
|
|
@ -1110,7 +1250,7 @@ Paths are relative to my memory directory. Frontmatter description is required o
|
||||||
"properties": {
|
"properties": {
|
||||||
"command": {
|
"command": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["read", "write", "append", "ls", "status", "init", "delete", "compact", "sync"],
|
"enum": ["read", "write", "append", "ls", "status", "init", "delete", "compact", "sync", "audit"],
|
||||||
"description": "What to do with my memory"
|
"description": "What to do with my memory"
|
||||||
},
|
},
|
||||||
"path": {
|
"path": {
|
||||||
|
|
@ -1340,4 +1480,124 @@ mod tests {
|
||||||
let after = std::fs::read_to_string(&commitments).unwrap();
|
let after = std::fs::read_to_string(&commitments).unwrap();
|
||||||
assert_eq!(before, after, "second init should not overwrite");
|
assert_eq!(before, after, "second init should not overwrite");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── description-required guard + audit ──────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_frontmatter_bound_scoping() {
|
||||||
|
// Frontmatter-bound: the description contract applies.
|
||||||
|
assert!(MemoryRepo::is_frontmatter_bound("system/persona"));
|
||||||
|
assert!(MemoryRepo::is_frontmatter_bound("reference/arch.md"));
|
||||||
|
assert!(MemoryRepo::is_frontmatter_bound("projects/plan.md"));
|
||||||
|
assert!(MemoryRepo::is_frontmatter_bound("issues/bug.md"));
|
||||||
|
|
||||||
|
// Different schemas that legitimately lack description.
|
||||||
|
assert!(!MemoryRepo::is_frontmatter_bound("tasks/some-todo.md"));
|
||||||
|
assert!(!MemoryRepo::is_frontmatter_bound("system/dynamic/itinerary.md"));
|
||||||
|
assert!(!MemoryRepo::is_frontmatter_bound("journal/2026/05/20.md"));
|
||||||
|
|
||||||
|
// Segment prefixes, not substring matches — a file named like a
|
||||||
|
// todo but living elsewhere is still bound.
|
||||||
|
assert!(MemoryRepo::is_frontmatter_bound("reference/tasks-today.md"));
|
||||||
|
assert!(MemoryRepo::is_frontmatter_bound("projects/journal-of-x.md"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_write_rejects_empty_description_on_bound_file() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let repo = MemoryRepo::new("test-agent", dir.path());
|
||||||
|
repo.init().await.unwrap();
|
||||||
|
|
||||||
|
// Seed a frontmatter-bound file with NO description (the gap).
|
||||||
|
std::fs::write(
|
||||||
|
repo.root().join("issues/gap.md"),
|
||||||
|
"---\nread_only: false\n---\nBody with no description\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// A body-only write must not silently perpetuate the empty description.
|
||||||
|
let err = repo.write("issues/gap.md", "updated body").await.unwrap_err();
|
||||||
|
assert!(
|
||||||
|
err.to_string().contains("no description"),
|
||||||
|
"expected loud rejection, got: {err}"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Supplying a description heals it and succeeds.
|
||||||
|
repo.write(
|
||||||
|
"issues/gap.md",
|
||||||
|
"---\ndescription: The healed headline\n---\nupdated body\n",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let healed = parse_memory_file(&std::fs::read_to_string(repo.root().join("issues/gap.md")).unwrap()).unwrap();
|
||||||
|
assert_eq!(healed.frontmatter.description, "The healed headline");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_write_allows_no_description_on_unbound_schemas() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let repo = MemoryRepo::new("test-agent", dir.path());
|
||||||
|
repo.init().await.unwrap();
|
||||||
|
|
||||||
|
// tasks/, journal/, system/dynamic/ legitimately lack description.
|
||||||
|
repo.write("tasks/a-todo.md", "todo body").await.unwrap();
|
||||||
|
repo.write("journal/2026/05/20.md", "freeform journal entry").await.unwrap();
|
||||||
|
repo.write("system/dynamic/itinerary.md", "itinerary body").await.unwrap();
|
||||||
|
|
||||||
|
// New bound files still get the auto-generated default on create.
|
||||||
|
repo.write("reference/new.md", "some reference").await.unwrap();
|
||||||
|
let f = parse_memory_file(&std::fs::read_to_string(repo.root().join("reference/new.md")).unwrap()).unwrap();
|
||||||
|
assert!(!f.frontmatter.description.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_append_rejects_empty_description_on_bound_file() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let repo = MemoryRepo::new("test-agent", dir.path());
|
||||||
|
repo.init().await.unwrap();
|
||||||
|
|
||||||
|
// Existing bound file missing description.
|
||||||
|
std::fs::write(
|
||||||
|
repo.root().join("projects/gap.md"),
|
||||||
|
"---\n---\nInitial body\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let err = repo.append("projects/gap.md", "more").await.unwrap_err();
|
||||||
|
assert!(err.to_string().contains("no description"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_audit_flags_only_bound_files_missing_description() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let repo = MemoryRepo::new("test-agent", dir.path());
|
||||||
|
repo.init().await.unwrap();
|
||||||
|
|
||||||
|
// Bound + missing description → flagged.
|
||||||
|
std::fs::write(
|
||||||
|
repo.root().join("issues/missing.md"),
|
||||||
|
"---\n---\nbody\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
// Bound + has description → not flagged.
|
||||||
|
std::fs::write(
|
||||||
|
repo.root().join("reference/has.md"),
|
||||||
|
"---\ndescription: present\n---\nbody\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
// Unbound schema + no description → not flagged (legitimate).
|
||||||
|
std::fs::write(
|
||||||
|
repo.root().join("tasks/todo.md"),
|
||||||
|
"---\nid: t1\n---\nbody\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
repo.root().join("journal/2026/05/20.md"),
|
||||||
|
"---\n---\nfreeform\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let missing = repo.audit().await.unwrap();
|
||||||
|
assert_eq!(missing, vec!["issues/missing.md".to_string()]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -256,6 +256,16 @@ fn handle_request(
|
||||||
}
|
}
|
||||||
Request::ShellReady => {
|
Request::ShellReady => {
|
||||||
let mut d = shared.lock();
|
let mut d = shared.lock();
|
||||||
|
|
||||||
|
// Session authority is an exclusive lease. A utility QML process
|
||||||
|
// or duplicate shell must never supersede the live supervised
|
||||||
|
// shell merely by sending shell_ready: doing so turns that
|
||||||
|
// process's EOF into a false shell-death lock. The current
|
||||||
|
// authority must disconnect first; its connection handler clears
|
||||||
|
// shell_alive before a replacement can register.
|
||||||
|
if d.shell_alive {
|
||||||
|
return refusal("shell authority is already registered");
|
||||||
|
}
|
||||||
d.heartbeat_gen += 1;
|
d.heartbeat_gen += 1;
|
||||||
d.shell_alive = true;
|
d.shell_alive = true;
|
||||||
let gen = d.heartbeat_gen;
|
let gen = d.heartbeat_gen;
|
||||||
|
|
@ -319,3 +329,52 @@ fn handle_request(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn idle_shared() -> Arc<Shared> {
|
||||||
|
Arc::new(Shared {
|
||||||
|
state: Mutex::new(Daemon {
|
||||||
|
phase: Phase::Idle,
|
||||||
|
controller: None,
|
||||||
|
heartbeat_gen: 0,
|
||||||
|
shell_alive: false,
|
||||||
|
pam_service: "test".to_string(),
|
||||||
|
}),
|
||||||
|
cond: Condvar::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn second_shell_ready_cannot_steal_authority_lease() {
|
||||||
|
let shared = idle_shared();
|
||||||
|
let mut authority_heartbeat = None;
|
||||||
|
let first = handle_request(
|
||||||
|
Request::ShellReady,
|
||||||
|
&shared,
|
||||||
|
&mut authority_heartbeat,
|
||||||
|
);
|
||||||
|
assert_eq!(first["ok"], true);
|
||||||
|
assert_eq!(authority_heartbeat, Some(1));
|
||||||
|
|
||||||
|
let mut utility_heartbeat = None;
|
||||||
|
let second = handle_request(
|
||||||
|
Request::ShellReady,
|
||||||
|
&shared,
|
||||||
|
&mut utility_heartbeat,
|
||||||
|
);
|
||||||
|
assert_eq!(second["ok"], false);
|
||||||
|
assert_eq!(
|
||||||
|
second["reason"],
|
||||||
|
"shell authority is already registered"
|
||||||
|
);
|
||||||
|
assert_eq!(utility_heartbeat, None);
|
||||||
|
|
||||||
|
let state = shared.lock();
|
||||||
|
assert!(state.shell_alive);
|
||||||
|
assert_eq!(state.heartbeat_gen, 1);
|
||||||
|
assert_eq!(state.phase, Phase::Released);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,12 @@ Singleton {
|
||||||
// true once the compositor has acknowledged the lock. Consumers that
|
// true once the compositor has acknowledged the lock. Consumers that
|
||||||
// disclose personal data must gate on secure, not merely on the request.
|
// disclose personal data must gate on secure, not merely on the request.
|
||||||
property bool screenLocked: false
|
property bool screenLocked: false
|
||||||
|
// True while the display is genuinely in use (IdleCoordinator Active or
|
||||||
|
// Waking). The idle machine drives it; widgets/pollers whose data may go
|
||||||
|
// stale while the screen is dimmed/locked/asleep gate their timers and
|
||||||
|
// processes on this instead of importing the souveraine IdleCoordinator
|
||||||
|
// singleton into ii-base (quickshell-idle-power task 4).
|
||||||
|
property bool displayActive: true
|
||||||
property bool screenLockSecure: false
|
property bool screenLockSecure: false
|
||||||
property bool screenLockContainsCharacters: false
|
property bool screenLockContainsCharacters: false
|
||||||
property bool screenUnlockFailed: false
|
property bool screenUnlockFailed: false
|
||||||
|
|
|
||||||
414
surfaces/quickshell/SettingsWindow.qml
Normal file
414
surfaces/quickshell/SettingsWindow.qml
Normal file
|
|
@ -0,0 +1,414 @@
|
||||||
|
//@ pragma UseQApplication
|
||||||
|
//@ pragma Env QS_NO_RELOAD_POPUP=1
|
||||||
|
//@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic
|
||||||
|
//@ pragma Env QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000
|
||||||
|
|
||||||
|
// Souveraine settings — one ordinary app window, owned by the authoritative
|
||||||
|
// shell process. Launchers call the `settings` IPC target; they never start a
|
||||||
|
// second Quickshell configuration or a second session-authority scope.
|
||||||
|
//
|
||||||
|
// Two form factors are gated on
|
||||||
|
// Config.options.souveraine.phone (the Device tab's "Phone mode" switch):
|
||||||
|
//
|
||||||
|
// phone === true → full-screen portrait stack navigation
|
||||||
|
// (list → push page → back), finger-sized rows.
|
||||||
|
// phone === false → a normal desktop window: a side rail of pages beside
|
||||||
|
// a content pane, both selecting from the SAME pages[].
|
||||||
|
//
|
||||||
|
// Both modes load identical page components (modules/settings/*Config.qml) via
|
||||||
|
// one Loader, so a page added once appears in both. Only the chrome differs.
|
||||||
|
// This replaces the old split of settings.qml (upstream desktop) +
|
||||||
|
// settings-phone.qml (our mobile) — one file is the source of truth now.
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Window
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.services
|
||||||
|
import qs.modules.common
|
||||||
|
import qs.modules.common.widgets
|
||||||
|
import qs.modules.common.functions as CF
|
||||||
|
|
||||||
|
ApplicationWindow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property bool phoneMode: Config.options.souveraine.phone
|
||||||
|
// -1 = list view (phone idle state). In desktop mode a page is always
|
||||||
|
// selected, so it starts at 0.
|
||||||
|
property int currentPage: Config.options.souveraine.phone ? -1 : 0
|
||||||
|
readonly property real rowHeight: 68 // finger-sized tap target
|
||||||
|
readonly property real edgeMargin: 16
|
||||||
|
|
||||||
|
// The page set — shared by both chromes. {name, icon, component}. One page
|
||||||
|
// per domain we own; About reuses ii's stock page verbatim.
|
||||||
|
property var pages: [
|
||||||
|
{
|
||||||
|
name: Translation.tr("Device"),
|
||||||
|
icon: "smartphone",
|
||||||
|
component: "modules/settings/DeviceConfig.qml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Translation.tr("Network & internet"),
|
||||||
|
icon: "wifi",
|
||||||
|
component: "modules/settings/NetworkConfig.qml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Translation.tr("Display"),
|
||||||
|
icon: "display_settings",
|
||||||
|
component: "modules/settings/DisplayConfig.qml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Translation.tr("Sound & microphone"),
|
||||||
|
icon: "volume_up",
|
||||||
|
component: "modules/settings/SoundConfig.qml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Translation.tr("Lock screen"),
|
||||||
|
icon: "lock",
|
||||||
|
component: "modules/settings/LockConfig.qml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Translation.tr("Wallpaper"),
|
||||||
|
icon: "wallpaper",
|
||||||
|
component: "modules/settings/WallpaperConfig.qml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Translation.tr("Dock"),
|
||||||
|
icon: "dock_to_bottom",
|
||||||
|
component: "modules/settings/DockConfig.qml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Translation.tr("Navigation"),
|
||||||
|
icon: "gesture",
|
||||||
|
component: "modules/settings/NavigationConfig.qml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Translation.tr("Idle & sleep"),
|
||||||
|
icon: "bedtime",
|
||||||
|
component: "modules/settings/IdleConfig.qml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Translation.tr("Keyboard"),
|
||||||
|
icon: "keyboard",
|
||||||
|
component: "modules/settings/KeyboardConfig.qml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Translation.tr("Speech"),
|
||||||
|
icon: "mic",
|
||||||
|
component: "modules/settings/SpeechConfig.qml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Translation.tr("About"),
|
||||||
|
icon: "info",
|
||||||
|
component: "modules/settings/About.qml"
|
||||||
|
}
|
||||||
|
// Next: app permissions/health, storage, accounts, updates.
|
||||||
|
]
|
||||||
|
|
||||||
|
visible: false
|
||||||
|
onClosing: close => {
|
||||||
|
// Closing Settings closes this app window, not the shell that owns it.
|
||||||
|
close.accepted = false
|
||||||
|
root.hideSettings()
|
||||||
|
}
|
||||||
|
title: "Settings"
|
||||||
|
color: Appearance.m3colors.m3background
|
||||||
|
|
||||||
|
// Phone windows deliberately start windowed and then take the exact same
|
||||||
|
// Hyprland fullscreen path as the navigation pill. Qt's Window.FullScreen
|
||||||
|
// sends a client fullscreen request instead; Hyprland treats that state
|
||||||
|
// differently from the pill's internal fullscreen dispatcher and the
|
||||||
|
// navigation rail can disappear. Keeping one compositor-owned path also
|
||||||
|
// means double-tapping the pill reliably returns Settings to windowed.
|
||||||
|
width: root.phoneMode ? Screen.width : 900
|
||||||
|
height: root.phoneMode ? Screen.height : 640
|
||||||
|
minimumWidth: root.phoneMode ? 0 : 640
|
||||||
|
minimumHeight: root.phoneMode ? 0 : 480
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
MaterialThemeLoader.reapplyTheme()
|
||||||
|
Config.readWriteDelay = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSettings() {
|
||||||
|
root.visible = true
|
||||||
|
root.raise()
|
||||||
|
root.requestActivate()
|
||||||
|
if (root.phoneMode) {
|
||||||
|
phoneFullscreenRetry.attempts = 0
|
||||||
|
phoneFullscreenRetry.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideSettings() {
|
||||||
|
root.visible = false
|
||||||
|
if (root.phoneMode) root.currentPage = -1
|
||||||
|
phoneFullscreenRetry.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSettings() {
|
||||||
|
if (root.visible) root.hideSettings()
|
||||||
|
else root.showSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "settings"
|
||||||
|
|
||||||
|
function open() { root.showSettings() }
|
||||||
|
function close() { root.hideSettings() }
|
||||||
|
function toggle() { root.toggleSettings() }
|
||||||
|
function state() {
|
||||||
|
return JSON.stringify({
|
||||||
|
visible: root.visible,
|
||||||
|
phone: root.phoneMode,
|
||||||
|
page: root.currentPage
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: phoneFullscreen
|
||||||
|
command: [
|
||||||
|
"hyprctl", "dispatch",
|
||||||
|
`hl.dsp.window.fullscreen({ window = "pid:${Quickshell.processId}", mode = "fullscreen", action = "set" })`
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// The QWindow maps just after Component.onCompleted. Retry briefly so a
|
||||||
|
// cold-started settings process cannot race its own Wayland toplevel.
|
||||||
|
Timer {
|
||||||
|
id: phoneFullscreenRetry
|
||||||
|
property int attempts: 0
|
||||||
|
interval: 180
|
||||||
|
repeat: true
|
||||||
|
onTriggered: {
|
||||||
|
attempts += 1
|
||||||
|
if (root.visible && !phoneFullscreen.running)
|
||||||
|
phoneFullscreen.running = true
|
||||||
|
if (attempts >= 3) stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// PHONE MODE — full-screen stack navigation
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
|
// --- List view (idle state) ------------------------------------------
|
||||||
|
ColumnLayout {
|
||||||
|
id: listView
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
visible: root.phoneMode && root.currentPage < 0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: root.edgeMargin
|
||||||
|
Layout.rightMargin: root.edgeMargin
|
||||||
|
Layout.topMargin: root.edgeMargin + 16
|
||||||
|
Layout.bottomMargin: 8
|
||||||
|
implicitHeight: titleText.implicitHeight
|
||||||
|
StyledText {
|
||||||
|
id: titleText
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: Appearance.colors.colOnLayer0
|
||||||
|
text: Translation.tr("Settings")
|
||||||
|
font {
|
||||||
|
family: Appearance.font.family.title
|
||||||
|
pixelSize: Appearance.font.pixelSize.title
|
||||||
|
variableAxes: Appearance.font.variableAxes.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledListView {
|
||||||
|
id: pageList
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
model: root.pages
|
||||||
|
delegate: RippleButton {
|
||||||
|
required property var index
|
||||||
|
required property var modelData
|
||||||
|
width: pageList.width
|
||||||
|
height: root.rowHeight
|
||||||
|
buttonRadius: 0
|
||||||
|
onClicked: root.currentPage = index
|
||||||
|
contentItem: RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: root.edgeMargin
|
||||||
|
anchors.rightMargin: root.edgeMargin
|
||||||
|
spacing: 16
|
||||||
|
MaterialSymbol {
|
||||||
|
text: modelData.icon
|
||||||
|
iconSize: Appearance.font.pixelSize.larger
|
||||||
|
color: Appearance.colors.colOnLayer0
|
||||||
|
}
|
||||||
|
StyledText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: modelData.name
|
||||||
|
color: Appearance.colors.colOnLayer0
|
||||||
|
font.pixelSize: Appearance.font.pixelSize.larger
|
||||||
|
}
|
||||||
|
MaterialSymbol {
|
||||||
|
text: "chevron_right"
|
||||||
|
iconSize: Appearance.font.pixelSize.larger
|
||||||
|
color: Appearance.colors.colSubtext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Page view (drilled-in state) ------------------------------------
|
||||||
|
Item {
|
||||||
|
id: pageView
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: root.phoneMode && root.currentPage >= 0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: pageHeader
|
||||||
|
anchors { top: parent.top; left: parent.left; right: parent.right }
|
||||||
|
height: 56
|
||||||
|
RippleButton {
|
||||||
|
id: backButton
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
leftMargin: 4
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
buttonRadius: Appearance.rounding.full
|
||||||
|
implicitWidth: 48
|
||||||
|
implicitHeight: 48
|
||||||
|
onClicked: root.currentPage = -1
|
||||||
|
contentItem: MaterialSymbol {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
text: "arrow_back"
|
||||||
|
iconSize: 24
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StyledText {
|
||||||
|
anchors {
|
||||||
|
left: backButton.right
|
||||||
|
leftMargin: 8
|
||||||
|
right: parent.right
|
||||||
|
rightMargin: root.edgeMargin
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
color: Appearance.colors.colOnLayer0
|
||||||
|
text: (root.currentPage >= 0 && root.currentPage < root.pages.length)
|
||||||
|
? root.pages[root.currentPage].name : ""
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font {
|
||||||
|
family: Appearance.font.family.title
|
||||||
|
pixelSize: Appearance.font.pixelSize.larger
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: phonePageLoader
|
||||||
|
anchors {
|
||||||
|
top: pageHeader.bottom
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
bottom: parent.bottom
|
||||||
|
}
|
||||||
|
active: root.phoneMode && root.currentPage >= 0 && Config.ready
|
||||||
|
source: (root.currentPage >= 0 && root.currentPage < root.pages.length)
|
||||||
|
? root.pages[root.currentPage].component : ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// DESKTOP MODE — side rail + content pane
|
||||||
|
// ====================================================================
|
||||||
|
RowLayout {
|
||||||
|
id: desktopView
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
visible: !root.phoneMode
|
||||||
|
|
||||||
|
// Left rail
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.preferredWidth: 220
|
||||||
|
color: Appearance.colors.colLayer1
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: root.edgeMargin + 8
|
||||||
|
Layout.leftMargin: root.edgeMargin
|
||||||
|
Layout.bottomMargin: 8
|
||||||
|
implicitHeight: railTitle.implicitHeight
|
||||||
|
StyledText {
|
||||||
|
id: railTitle
|
||||||
|
anchors.left: parent.left
|
||||||
|
text: Translation.tr("Settings")
|
||||||
|
color: Appearance.colors.colOnLayer1
|
||||||
|
font {
|
||||||
|
family: Appearance.font.family.title
|
||||||
|
pixelSize: Appearance.font.pixelSize.larger
|
||||||
|
variableAxes: Appearance.font.variableAxes.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledListView {
|
||||||
|
id: railList
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
model: root.pages
|
||||||
|
delegate: RippleButton {
|
||||||
|
required property var index
|
||||||
|
required property var modelData
|
||||||
|
width: railList.width
|
||||||
|
height: 44
|
||||||
|
buttonRadius: 0
|
||||||
|
toggled: root.currentPage === index
|
||||||
|
colBackgroundToggled: Appearance.colors.colSecondaryContainer
|
||||||
|
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
|
||||||
|
colRippleToggled: Appearance.colors.colSecondaryContainerActive
|
||||||
|
onClicked: root.currentPage = index
|
||||||
|
contentItem: RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: root.edgeMargin
|
||||||
|
anchors.rightMargin: 8
|
||||||
|
spacing: 12
|
||||||
|
MaterialSymbol {
|
||||||
|
text: modelData.icon
|
||||||
|
iconSize: Appearance.font.pixelSize.large
|
||||||
|
color: root.currentPage === index
|
||||||
|
? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1
|
||||||
|
}
|
||||||
|
StyledText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: modelData.name
|
||||||
|
elide: Text.ElideRight
|
||||||
|
color: root.currentPage === index
|
||||||
|
? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1
|
||||||
|
font.pixelSize: Appearance.font.pixelSize.normal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content pane
|
||||||
|
Loader {
|
||||||
|
id: desktopPageLoader
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
active: !root.phoneMode && root.currentPage >= 0 && Config.ready
|
||||||
|
source: (root.currentPage >= 0 && root.currentPage < root.pages.length)
|
||||||
|
? root.pages[root.currentPage].component : ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
670
surfaces/quickshell/assets/keyboard/us.json
Normal file
670
surfaces/quickshell/assets/keyboard/us.json
Normal file
|
|
@ -0,0 +1,670 @@
|
||||||
|
{
|
||||||
|
"levels": [
|
||||||
|
{
|
||||||
|
"level": "",
|
||||||
|
"mode": "default",
|
||||||
|
"rows": [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"1",
|
||||||
|
"¹",
|
||||||
|
"½",
|
||||||
|
"⅓",
|
||||||
|
"¼",
|
||||||
|
"⅛"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"2",
|
||||||
|
"²",
|
||||||
|
"⅔"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"3",
|
||||||
|
"³",
|
||||||
|
"¾",
|
||||||
|
"⅜"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"4",
|
||||||
|
"⁴"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"5",
|
||||||
|
"⅝"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"6"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"7",
|
||||||
|
"⅞"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"8"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"9"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"0",
|
||||||
|
"ⁿ",
|
||||||
|
"∅"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"q"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"w"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"e",
|
||||||
|
"è",
|
||||||
|
"é",
|
||||||
|
"ê",
|
||||||
|
"ë",
|
||||||
|
"ē"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"t"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"y"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"u",
|
||||||
|
"û",
|
||||||
|
"ü",
|
||||||
|
"ù",
|
||||||
|
"ú",
|
||||||
|
"ū"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"i",
|
||||||
|
"î",
|
||||||
|
"ï",
|
||||||
|
"í",
|
||||||
|
"ī",
|
||||||
|
"ì"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"o",
|
||||||
|
"ô",
|
||||||
|
"ö",
|
||||||
|
"ò",
|
||||||
|
"ó",
|
||||||
|
"œ",
|
||||||
|
"ø",
|
||||||
|
"ō",
|
||||||
|
"õ"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"p"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"a",
|
||||||
|
"à",
|
||||||
|
"á",
|
||||||
|
"â",
|
||||||
|
"ä",
|
||||||
|
"æ",
|
||||||
|
"ã",
|
||||||
|
"å",
|
||||||
|
"ā"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"s",
|
||||||
|
"ß"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"d"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"f"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"g"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"h"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"j"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"k"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"l"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"z"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"x"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"c",
|
||||||
|
"ç"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"v"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"b"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"n",
|
||||||
|
"ñ"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"m"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
","
|
||||||
|
],
|
||||||
|
[
|
||||||
|
" "
|
||||||
|
],
|
||||||
|
[
|
||||||
|
".",
|
||||||
|
"#",
|
||||||
|
"!",
|
||||||
|
",",
|
||||||
|
"?",
|
||||||
|
"-",
|
||||||
|
":",
|
||||||
|
"'",
|
||||||
|
"@"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": "shift",
|
||||||
|
"mode": "latched",
|
||||||
|
"rows": [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"1",
|
||||||
|
"¹",
|
||||||
|
"½",
|
||||||
|
"⅓",
|
||||||
|
"¼",
|
||||||
|
"⅛"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"2",
|
||||||
|
"²",
|
||||||
|
"⅔"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"3",
|
||||||
|
"³",
|
||||||
|
"¾",
|
||||||
|
"⅜"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"4",
|
||||||
|
"⁴"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"5",
|
||||||
|
"⅝"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"6"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"7",
|
||||||
|
"⅞"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"8"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"9"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"0",
|
||||||
|
"ⁿ",
|
||||||
|
"∅"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"Q"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"W"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"E",
|
||||||
|
"È",
|
||||||
|
"É",
|
||||||
|
"Ê",
|
||||||
|
"Ë",
|
||||||
|
"Ē"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"R"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"T"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Y"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"U",
|
||||||
|
"Û",
|
||||||
|
"Ü",
|
||||||
|
"Ù",
|
||||||
|
"Ú",
|
||||||
|
"Ū"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"I",
|
||||||
|
"Î",
|
||||||
|
"Ï",
|
||||||
|
"Í",
|
||||||
|
"Ī",
|
||||||
|
"Ì"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"O",
|
||||||
|
"Ô",
|
||||||
|
"Ö",
|
||||||
|
"Ò",
|
||||||
|
"Ó",
|
||||||
|
"Œ",
|
||||||
|
"Ø",
|
||||||
|
"Ō",
|
||||||
|
"Õ"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"P"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"A",
|
||||||
|
"À",
|
||||||
|
"Á",
|
||||||
|
"Â",
|
||||||
|
"Ä",
|
||||||
|
"Æ",
|
||||||
|
"Ã",
|
||||||
|
"Å",
|
||||||
|
"Ā"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"S",
|
||||||
|
"ẞ",
|
||||||
|
"SS"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"D"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"F"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"G"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"H"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"J"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"K"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"L"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"Z"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"X"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"C",
|
||||||
|
"Ç"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"V"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"B"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"N",
|
||||||
|
"Ñ"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"M"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
","
|
||||||
|
],
|
||||||
|
[
|
||||||
|
" "
|
||||||
|
],
|
||||||
|
[
|
||||||
|
".",
|
||||||
|
"#",
|
||||||
|
"!",
|
||||||
|
",",
|
||||||
|
"?",
|
||||||
|
"-",
|
||||||
|
":",
|
||||||
|
"'",
|
||||||
|
"@"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": "opt",
|
||||||
|
"mode": "locked",
|
||||||
|
"rows": [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"1",
|
||||||
|
"¹",
|
||||||
|
"½",
|
||||||
|
"⅓",
|
||||||
|
"¼",
|
||||||
|
"⅛"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"2",
|
||||||
|
"²",
|
||||||
|
"⅔"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"3",
|
||||||
|
"³",
|
||||||
|
"¾",
|
||||||
|
"⅜"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"4",
|
||||||
|
"⁴"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"5",
|
||||||
|
"⅝"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"6"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"7",
|
||||||
|
"⅞"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"8"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"9"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"0",
|
||||||
|
"ⁿ",
|
||||||
|
"∅"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"@"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"#"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"$",
|
||||||
|
"¢",
|
||||||
|
"£",
|
||||||
|
"€",
|
||||||
|
"¥",
|
||||||
|
"₱"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"%",
|
||||||
|
"‰"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"&"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"-",
|
||||||
|
"_",
|
||||||
|
"–",
|
||||||
|
"—",
|
||||||
|
"·"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"+",
|
||||||
|
"±"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"(",
|
||||||
|
"<",
|
||||||
|
"{",
|
||||||
|
"["
|
||||||
|
],
|
||||||
|
[
|
||||||
|
")",
|
||||||
|
">",
|
||||||
|
"}",
|
||||||
|
"]"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"*",
|
||||||
|
"†",
|
||||||
|
"‡",
|
||||||
|
"★"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"\"",
|
||||||
|
"“",
|
||||||
|
"”",
|
||||||
|
"«",
|
||||||
|
"»"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"'",
|
||||||
|
"‘",
|
||||||
|
"’",
|
||||||
|
"‹",
|
||||||
|
"›"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
":"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
";"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"!",
|
||||||
|
"¡"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"?",
|
||||||
|
"¿"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"_"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"/"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
" "
|
||||||
|
],
|
||||||
|
[
|
||||||
|
","
|
||||||
|
],
|
||||||
|
[
|
||||||
|
".",
|
||||||
|
"…"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": "opt+shift",
|
||||||
|
"mode": "locked",
|
||||||
|
"rows": [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"~"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"`"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"|"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"•",
|
||||||
|
"♪",
|
||||||
|
"♥",
|
||||||
|
"♠",
|
||||||
|
"♦",
|
||||||
|
"♣"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"√"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Π",
|
||||||
|
"π"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"÷"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"×"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"¶",
|
||||||
|
"§"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"∆"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"£"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"¢"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"€"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"¥"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"^",
|
||||||
|
"↑",
|
||||||
|
"↓",
|
||||||
|
"←",
|
||||||
|
"→"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"°",
|
||||||
|
"′",
|
||||||
|
"″"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"=",
|
||||||
|
"≠",
|
||||||
|
"≈",
|
||||||
|
"∞"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"{"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"}"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"\\"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"©"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"®"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"™"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"℅"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"["
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"]"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"<",
|
||||||
|
"‹",
|
||||||
|
"≤",
|
||||||
|
"«"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
">",
|
||||||
|
"›",
|
||||||
|
"≥",
|
||||||
|
"»"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
" "
|
||||||
|
],
|
||||||
|
[
|
||||||
|
","
|
||||||
|
],
|
||||||
|
[
|
||||||
|
".",
|
||||||
|
"…"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"locale": "en",
|
||||||
|
"name": "English (US) — number row"
|
||||||
|
}
|
||||||
|
|
@ -31,8 +31,7 @@ TARGET_ARCH="${SOUVERAINE_TARGET_ARCH:-$(uname -m)}"
|
||||||
MANIFEST="
|
MANIFEST="
|
||||||
shell.qml souveraine/shell.qml
|
shell.qml souveraine/shell.qml
|
||||||
GlobalStates.qml souveraine/GlobalStates.qml
|
GlobalStates.qml souveraine/GlobalStates.qml
|
||||||
settings.qml souveraine/settings.qml
|
SettingsWindow.qml souveraine/SettingsWindow.qml
|
||||||
settings-phone.qml souveraine/settings-phone.qml
|
|
||||||
panelFamilies/SouveraineFamily.qml souveraine/panelFamilies/SouveraineFamily.qml
|
panelFamilies/SouveraineFamily.qml souveraine/panelFamilies/SouveraineFamily.qml
|
||||||
services/Souveraine.qml souveraine/services/Souveraine.qml
|
services/Souveraine.qml souveraine/services/Souveraine.qml
|
||||||
services/Ai.qml souveraine/services/Ai.qml
|
services/Ai.qml souveraine/services/Ai.qml
|
||||||
|
|
@ -57,6 +56,7 @@ services/CrashReporter.qml souveraine/services/CrashReporter.qml
|
||||||
modules/ii/sidebarLeft/SidebarLeft.qml souveraine/modules/ii/sidebarLeft/SidebarLeft.qml
|
modules/ii/sidebarLeft/SidebarLeft.qml souveraine/modules/ii/sidebarLeft/SidebarLeft.qml
|
||||||
modules/ii/sidebarLeft/AiChat.qml souveraine/modules/ii/sidebarLeft/AiChat.qml
|
modules/ii/sidebarLeft/AiChat.qml souveraine/modules/ii/sidebarLeft/AiChat.qml
|
||||||
modules/ii/sidebarRight/SidebarRight.qml souveraine/modules/ii/sidebarRight/SidebarRight.qml
|
modules/ii/sidebarRight/SidebarRight.qml souveraine/modules/ii/sidebarRight/SidebarRight.qml
|
||||||
|
modules/ii/sidebarRight/SidebarRightContent.qml souveraine/modules/ii/sidebarRight/SidebarRightContent.qml
|
||||||
modules/ii/sidebarRight/QuickSliders.qml souveraine/modules/ii/sidebarRight/QuickSliders.qml
|
modules/ii/sidebarRight/QuickSliders.qml souveraine/modules/ii/sidebarRight/QuickSliders.qml
|
||||||
modules/ii/sidebarRight/volumeMixer/VolumeDialogContent.qml souveraine/modules/ii/sidebarRight/volumeMixer/VolumeDialogContent.qml
|
modules/ii/sidebarRight/volumeMixer/VolumeDialogContent.qml souveraine/modules/ii/sidebarRight/volumeMixer/VolumeDialogContent.qml
|
||||||
modules/ii/bar/UtilButtons.qml souveraine/modules/ii/bar/UtilButtons.qml
|
modules/ii/bar/UtilButtons.qml souveraine/modules/ii/bar/UtilButtons.qml
|
||||||
|
|
@ -66,6 +66,9 @@ modules/common/functions/Session.qml souveraine/modules/common/functions/Session
|
||||||
modules/common/widgets/ContentPage.qml souveraine/modules/common/widgets/ContentPage.qml
|
modules/common/widgets/ContentPage.qml souveraine/modules/common/widgets/ContentPage.qml
|
||||||
modules/common/widgets/StyledToolTip.qml souveraine/modules/common/widgets/StyledToolTip.qml
|
modules/common/widgets/StyledToolTip.qml souveraine/modules/common/widgets/StyledToolTip.qml
|
||||||
modules/settings/DeviceConfig.qml souveraine/modules/settings/DeviceConfig.qml
|
modules/settings/DeviceConfig.qml souveraine/modules/settings/DeviceConfig.qml
|
||||||
|
modules/settings/NetworkConfig.qml souveraine/modules/settings/NetworkConfig.qml
|
||||||
|
modules/settings/DisplayConfig.qml souveraine/modules/settings/DisplayConfig.qml
|
||||||
|
modules/settings/SoundConfig.qml souveraine/modules/settings/SoundConfig.qml
|
||||||
modules/settings/LockConfig.qml souveraine/modules/settings/LockConfig.qml
|
modules/settings/LockConfig.qml souveraine/modules/settings/LockConfig.qml
|
||||||
modules/settings/WallpaperConfig.qml souveraine/modules/settings/WallpaperConfig.qml
|
modules/settings/WallpaperConfig.qml souveraine/modules/settings/WallpaperConfig.qml
|
||||||
modules/settings/DockConfig.qml souveraine/modules/settings/DockConfig.qml
|
modules/settings/DockConfig.qml souveraine/modules/settings/DockConfig.qml
|
||||||
|
|
@ -159,6 +162,62 @@ fi
|
||||||
|
|
||||||
if [[ "${1:-}" == "--phone" ]]; then
|
if [[ "${1:-}" == "--phone" ]]; then
|
||||||
ssh_i=(ssh -F /dev/null -i "$HOME/.ssh/ani" -o ConnectTimeout=5)
|
ssh_i=(ssh -F /dev/null -i "$HOME/.ssh/ani" -o ConnectTimeout=5)
|
||||||
|
maintenance_active=0
|
||||||
|
hypridle_was_active=0
|
||||||
|
|
||||||
|
phone_lock_state() {
|
||||||
|
local state last_secure
|
||||||
|
state="$("${ssh_i[@]}" "$PHONE_USB" \
|
||||||
|
"qs -c souveraine ipc --any-display call session state 2>/dev/null" || true)"
|
||||||
|
if grep -Eq '"locked"[[:space:]]*:[[:space:]]*false' <<< "$state" \
|
||||||
|
&& grep -Eq '"lockRequested"[[:space:]]*:[[:space:]]*false' <<< "$state"; then
|
||||||
|
printf '%s\n' unlocked
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if grep -Eq '"locked"[[:space:]]*:[[:space:]]*true|"lockRequested"[[:space:]]*:[[:space:]]*true' <<< "$state"; then
|
||||||
|
printf '%s\n' locked
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Bootstrap for a phone that predates the session IPC handler. Every
|
||||||
|
# compositor-acknowledged lock edge is journaled by LockScreen.qml.
|
||||||
|
last_secure="$("${ssh_i[@]}" "$PHONE_USB" \
|
||||||
|
"journalctl --user -u souveraine-shell.service -b --no-pager | sed -n 's/.*\\[lock\\] session lock secure=\\(true\\|false\\).*/\\1/p' | tail -n 1" || true)"
|
||||||
|
[[ "$last_secure" == "false" ]] && printf '%s\n' unlocked || printf '%s\n' locked
|
||||||
|
}
|
||||||
|
|
||||||
|
finish_phone_maintenance() {
|
||||||
|
(( maintenance_active )) || return 0
|
||||||
|
if (( hypridle_was_active )); then
|
||||||
|
"${ssh_i[@]}" "$PHONE_USB" \
|
||||||
|
"systemctl --user restart hypridle.service; systemctl --user stop souveraine-deploy-lease.timer 2>/dev/null || true; systemctl --user reset-failed souveraine-deploy-lease.service 2>/dev/null || true" \
|
||||||
|
>/dev/null || true
|
||||||
|
fi
|
||||||
|
maintenance_active=0
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$(phone_lock_state)" != "unlocked" ]]; then
|
||||||
|
echo "ERROR: phone session is locked; refusing a live shell deploy" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if "${ssh_i[@]}" "$PHONE_USB" "systemctl --user is-active --quiet hypridle.service"; then
|
||||||
|
hypridle_was_active=1
|
||||||
|
# Schedule recovery before stopping idle handling. If this deploy or
|
||||||
|
# its SSH connection dies, systemd restores hypridle within five
|
||||||
|
# minutes. Manual lock remains available throughout the lease.
|
||||||
|
"${ssh_i[@]}" "$PHONE_USB" \
|
||||||
|
"systemctl --user stop souveraine-deploy-lease.timer 2>/dev/null || true; systemd-run --user --unit=souveraine-deploy-lease --on-active=5min --timer-property=AccuracySec=1s --collect /usr/bin/systemctl --user restart hypridle.service >/dev/null; systemctl --user stop hypridle.service"
|
||||||
|
fi
|
||||||
|
maintenance_active=1
|
||||||
|
trap finish_phone_maintenance EXIT
|
||||||
|
|
||||||
|
# Close the small check→inhibit race. A manual lock always wins.
|
||||||
|
if [[ "$(phone_lock_state)" != "unlocked" ]]; then
|
||||||
|
echo "ERROR: phone locked while acquiring the maintenance lease" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
deployed_revision="unknown"
|
deployed_revision="unknown"
|
||||||
if repo_root="$(git -C "$SRC" rev-parse --show-toplevel 2>/dev/null)"; then
|
if repo_root="$(git -C "$SRC" rev-parse --show-toplevel 2>/dev/null)"; then
|
||||||
deployed_revision="$(git -C "$repo_root" rev-parse HEAD)"
|
deployed_revision="$(git -C "$repo_root" rev-parse HEAD)"
|
||||||
|
|
@ -174,6 +233,8 @@ if [[ "${1:-}" == "--phone" ]]; then
|
||||||
"${ssh_i[@]}" "$PHONE_USB" "bash ~/$PHONE_DEST/deploy.sh"
|
"${ssh_i[@]}" "$PHONE_USB" "bash ~/$PHONE_DEST/deploy.sh"
|
||||||
# Phone-only: install the mobile settings launcher into the app grid.
|
# Phone-only: install the mobile settings launcher into the app grid.
|
||||||
"${ssh_i[@]}" "$PHONE_USB" "mkdir -p ~/.local/share/applications && ln -sf ~/$PHONE_DEST/souveraine-settings.desktop ~/.local/share/applications/souveraine-settings.desktop"
|
"${ssh_i[@]}" "$PHONE_USB" "mkdir -p ~/.local/share/applications && ln -sf ~/$PHONE_DEST/souveraine-settings.desktop ~/.local/share/applications/souveraine-settings.desktop"
|
||||||
|
finish_phone_maintenance
|
||||||
|
trap - EXIT
|
||||||
echo "Deployed to phone. Hyprland starts qs -c souveraine; no secondary shell is required."
|
echo "Deployed to phone. Hyprland starts qs -c souveraine; no secondary shell is required."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,33 @@ Singleton {
|
||||||
return str.replace(/\\/g, '\\\\');
|
return str.replace(/\\/g, '\\\\');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans assistant reply text for text-to-speech: strips <think> blocks,
|
||||||
|
* code fences, and markdown formatting so only the spoken voice remains.
|
||||||
|
* Returns the cleaned text (empty string if nothing speakable survives).
|
||||||
|
* @param { string } text
|
||||||
|
* @returns { string }
|
||||||
|
*/
|
||||||
|
function ttsClean(text) {
|
||||||
|
if (!text)
|
||||||
|
return "";
|
||||||
|
return text
|
||||||
|
// Remove <think>...</think> blocks entirely
|
||||||
|
.replace(/<think>[\s\S]*?<\/think>/g, "")
|
||||||
|
// Remove markdown headers, bold, italic, code fences
|
||||||
|
.replace(/```[\s\S]*?```/g, " ")
|
||||||
|
.replace(/#{1,6}\s/g, " ")
|
||||||
|
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
||||||
|
.replace(/\*([^*]+)\*/g, "$1")
|
||||||
|
.replace(/`([^`]+)`/g, "$1")
|
||||||
|
// Remove markdown links and images
|
||||||
|
.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1")
|
||||||
|
.replace(/\[([^\]]*)\]\([^)]+\)/g, "$1")
|
||||||
|
// Collapse whitespace
|
||||||
|
.replace(/\n{3,}/g, "\n\n")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps words to supplied maximum length
|
* Wraps words to supplied maximum length
|
||||||
* @param { string | null } str
|
* @param { string | null } str
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,22 @@ AbstractBackgroundWidget {
|
||||||
"data_format = ascii\nascii_max_range = 100\n";
|
"data_format = ascii\nascii_max_range = 100\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Souveraine (idle-power task 4): cava only earns its keep while the
|
||||||
|
// widget can actually be seen. Gate the watchdog on visibility + display
|
||||||
|
// activity, and tear cava down when either goes away.
|
||||||
|
readonly property bool wantCava: parent.visible && GlobalStates.displayActive
|
||||||
|
onWantCavaChanged: {
|
||||||
|
if (!wantCava && cavaProcess.running) {
|
||||||
|
root.cavaReady = false;
|
||||||
|
cavaProcess.running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Watchdog: every 2s, if cava isn't running, write config and start it
|
// Watchdog: every 2s, if cava isn't running, write config and start it
|
||||||
Timer {
|
Timer {
|
||||||
id: watchdog
|
id: watchdog
|
||||||
interval: 2000
|
interval: 2000
|
||||||
running: true
|
running: root.wantCava
|
||||||
repeat: true
|
repeat: true
|
||||||
triggeredOnStart: true
|
triggeredOnStart: true
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
|
|
@ -190,7 +201,14 @@ AbstractBackgroundWidget {
|
||||||
}
|
}
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
Timer { interval: 33; running: root.style === "wave"; repeat: true; onTriggered: parent.requestPaint() }
|
// 30fps repaint only while this canvas is actually visible.
|
||||||
|
// Was `running: root.style === "wave"` — but the Canvas's own
|
||||||
|
// `visible` (line above) already encodes that, AND collapses to
|
||||||
|
// false when the widget subtree is hidden. Gating the timer on
|
||||||
|
// `parent.visible` stops the continuous redraw whenever the
|
||||||
|
// canvas isn't shown, instead of painting behind a hidden
|
||||||
|
// background. No new import needed (pure QtQuick `visible`).
|
||||||
|
Timer { interval: 33; running: parent.visible; repeat: true; onTriggered: parent.requestPaint() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ Singleton {
|
||||||
{ identifier: "resources", materialSymbol: "browse_activity" },
|
{ identifier: "resources", materialSymbol: "browse_activity" },
|
||||||
{ identifier: "notes", materialSymbol: "note_stack" },
|
{ identifier: "notes", materialSymbol: "note_stack" },
|
||||||
{ identifier: "volumeMixer", materialSymbol: "volume_up" },
|
{ identifier: "volumeMixer", materialSymbol: "volume_up" },
|
||||||
|
{ identifier: "subconsciousEventPanel", materialSymbol: "psychology" },
|
||||||
]
|
]
|
||||||
|
|
||||||
readonly property bool hasPinnedWidgets: root.pinnedWidgetIdentifiers.length > 0
|
readonly property bool hasPinnedWidgets: root.pinnedWidgetIdentifiers.length > 0
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import qs.modules.ii.overlay.fpsLimiter
|
||||||
import qs.modules.ii.overlay.recorder
|
import qs.modules.ii.overlay.recorder
|
||||||
import qs.modules.ii.overlay.resources
|
import qs.modules.ii.overlay.resources
|
||||||
import qs.modules.ii.overlay.notes
|
import qs.modules.ii.overlay.notes
|
||||||
|
import qs.modules.ii.overlay.subconsciousEventPanel
|
||||||
|
|
||||||
DelegateChooser {
|
DelegateChooser {
|
||||||
id: root
|
id: root
|
||||||
|
|
@ -25,4 +26,5 @@ DelegateChooser {
|
||||||
DelegateChoice { roleValue: "resources"; Resources {} }
|
DelegateChoice { roleValue: "resources"; Resources {} }
|
||||||
DelegateChoice { roleValue: "notes"; Notes {} }
|
DelegateChoice { roleValue: "notes"; Notes {} }
|
||||||
DelegateChoice { roleValue: "volumeMixer"; VolumeMixer {} }
|
DelegateChoice { roleValue: "volumeMixer"; VolumeMixer {} }
|
||||||
|
DelegateChoice { roleValue: "subconsciousEventPanel"; SubconsciousEventPanel {} }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs
|
||||||
|
import qs.services
|
||||||
|
import qs.modules.common
|
||||||
|
import qs.modules.common.widgets
|
||||||
|
import qs.modules.common.functions
|
||||||
|
import qs.modules.ii.overlay
|
||||||
|
|
||||||
|
// Tier 2 of the subconscious three-tier visibility design (see
|
||||||
|
// docs/tasks/subconscious-surfacing-threshold.md): a persistent, interactive
|
||||||
|
// event log — the shell's equivalent of the TUI "cockpit". Shows surfaced
|
||||||
|
// subconscious events (pass snapshots, reflections, archivist syntheses,
|
||||||
|
// surfacings) from past turns. Each entry click-expands to its full text,
|
||||||
|
// closing docs/bugs.md B-005 ("subconscious messages not expandable").
|
||||||
|
// Fed by Ai.subconsciousEvents; the live Tier-1 ticker is snapshotted into
|
||||||
|
// it on pass end.
|
||||||
|
StyledOverlayWidget {
|
||||||
|
id: root
|
||||||
|
title: Translation.tr("Subconscious")
|
||||||
|
showCenterButton: true
|
||||||
|
|
||||||
|
contentItem: Rectangle {
|
||||||
|
radius: root.contentRadius
|
||||||
|
color: Appearance.colors.colLayer1
|
||||||
|
implicitWidth: 340
|
||||||
|
implicitHeight: 420
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 8
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
// Header
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
MaterialSymbol {
|
||||||
|
iconSize: Appearance.font.pixelSize.larger
|
||||||
|
text: "psychology"
|
||||||
|
color: Appearance.colors.colSubtext
|
||||||
|
}
|
||||||
|
StyledText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: Translation.tr("Subconscious event log")
|
||||||
|
color: Appearance.colors.colSubtext
|
||||||
|
font.pixelSize: Appearance.font.pixelSize.small
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
StyledText {
|
||||||
|
text: Ai.subconsciousEvents.length
|
||||||
|
color: Appearance.colors.colSubtext
|
||||||
|
font.pixelSize: Appearance.font.pixelSize.small
|
||||||
|
visible: Ai.subconsciousEvents.length > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event list
|
||||||
|
ScrollView {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: eventList
|
||||||
|
model: Ai.subconsciousEvents
|
||||||
|
spacing: 4
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: eventList.width
|
||||||
|
height: entryCol.implicitHeight + 12
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
color: Appearance.colors.colLayer2
|
||||||
|
border.width: 1
|
||||||
|
border.color: Appearance.colors.colLayer1Inactive
|
||||||
|
|
||||||
|
property bool expanded: false
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: entryCol
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 6
|
||||||
|
spacing: 3
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
MaterialSymbol {
|
||||||
|
iconSize: Appearance.font.pixelSize.small
|
||||||
|
text: modelData.kind === "halt" ? "error"
|
||||||
|
: modelData.kind === "reflection" ? "lightbulb"
|
||||||
|
: modelData.kind === "archivist" ? "auto_stories"
|
||||||
|
: "psychology"
|
||||||
|
color: Appearance.colors.colSubtext
|
||||||
|
}
|
||||||
|
StyledText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: modelData.source ?? Translation.tr("Subconscious")
|
||||||
|
color: Appearance.colors.colOnLayer1
|
||||||
|
font.pixelSize: Appearance.font.pixelSize.small
|
||||||
|
font.bold: true
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
MaterialSymbol {
|
||||||
|
iconSize: Appearance.font.pixelSize.small
|
||||||
|
text: expanded ? "expand_less" : "expand_more"
|
||||||
|
color: Appearance.colors.colSubtext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: !expanded
|
||||||
|
text: (modelData.content ?? "").split("\n")[0]
|
||||||
|
color: Appearance.colors.colSubtext
|
||||||
|
font.pixelSize: Appearance.font.pixelSize.small
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: expanded
|
||||||
|
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||||
|
text: modelData.content ?? ""
|
||||||
|
color: Appearance.colors.colOnLayer1
|
||||||
|
font.pixelSize: Appearance.font.pixelSize.small
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: parent.expanded = !parent.expanded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty state
|
||||||
|
StyledText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
visible: Ai.subconsciousEvents.length === 0
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
text: Translation.tr("No subconscious events yet.\nThey appear here after a pass.")
|
||||||
|
color: Appearance.colors.colSubtext
|
||||||
|
font.pixelSize: Appearance.font.pixelSize.small
|
||||||
|
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear log
|
||||||
|
StyledText {
|
||||||
|
Layout.alignment: Qt.AlignRight
|
||||||
|
text: Translation.tr("Clear")
|
||||||
|
color: Appearance.colors.colSubtext
|
||||||
|
font.pixelSize: Appearance.font.pixelSize.small
|
||||||
|
visible: Ai.subconsciousEvents.length > 0
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: Ai.subconsciousEvents = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -66,6 +66,7 @@ RowLayout {
|
||||||
focus: GlobalStates.overviewOpen
|
focus: GlobalStates.overviewOpen
|
||||||
font.pixelSize: Appearance.font.pixelSize.small
|
font.pixelSize: Appearance.font.pixelSize.small
|
||||||
placeholderText: Translation.tr("Search, calculate or run")
|
placeholderText: Translation.tr("Search, calculate or run")
|
||||||
|
inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase
|
||||||
implicitWidth: root.searchingText == "" ? Appearance.sizes.searchWidthCollapsed : Appearance.sizes.searchWidth
|
implicitWidth: root.searchingText == "" ? Appearance.sizes.searchWidthCollapsed : Appearance.sizes.searchWidth
|
||||||
|
|
||||||
Behavior on implicitWidth {
|
Behavior on implicitWidth {
|
||||||
|
|
@ -78,7 +79,14 @@ RowLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onTextChanged: LauncherSearch.query = text
|
// Souveraine: drive the search from committed text PLUS live
|
||||||
|
// input-method composition. Stevia (sm.puri.OSK0) delivers typed
|
||||||
|
// letters as pre-edit that it may never commit, so `text` alone stays
|
||||||
|
// empty while the letters are visibly in the field — hardware
|
||||||
|
// keyboards and IPC setQuery worked, OSK typing searched nothing
|
||||||
|
// (observed 2026-07-22: field showed letters, query stayed "").
|
||||||
|
onTextChanged: LauncherSearch.query = text + preeditText
|
||||||
|
onPreeditTextChanged: LauncherSearch.query = text + preeditText
|
||||||
|
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
if (appResults.count > 0) {
|
if (appResults.count > 0) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import Qt.labs.synchronizer
|
import Qt.labs.synchronizer
|
||||||
import Qt5Compat.GraphicalEffects
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
|
@ -47,6 +46,25 @@ Item { // Wrapper
|
||||||
LauncherSearch.query = text;
|
LauncherSearch.query = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function debugState() {
|
||||||
|
return {
|
||||||
|
showResults: root.showResults,
|
||||||
|
width: root.width,
|
||||||
|
height: root.height,
|
||||||
|
contentWidth: searchWidgetContent.width,
|
||||||
|
contentHeight: searchWidgetContent.height,
|
||||||
|
columnWidth: columnLayout.width,
|
||||||
|
columnHeight: columnLayout.height,
|
||||||
|
listVisible: appResults.visible,
|
||||||
|
listCount: appResults.count,
|
||||||
|
listWidth: appResults.width,
|
||||||
|
listHeight: appResults.height,
|
||||||
|
listImplicitHeight: appResults.implicitHeight,
|
||||||
|
listContentHeight: appResults.contentHeight,
|
||||||
|
modelCount: resultModel.count
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Keys.onPressed: event => {
|
Keys.onPressed: event => {
|
||||||
// Prevent Esc and Backspace from registering
|
// Prevent Esc and Backspace from registering
|
||||||
if (event.key === Qt.Key_Escape)
|
if (event.key === Qt.Key_Escape)
|
||||||
|
|
@ -146,16 +164,6 @@ Item { // Wrapper
|
||||||
}
|
}
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
// clip: true
|
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: OpacityMask {
|
|
||||||
maskSource: Rectangle {
|
|
||||||
width: searchWidgetContent.width
|
|
||||||
height: searchWidgetContent.width
|
|
||||||
radius: searchWidgetContent.radius
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SearchBar {
|
SearchBar {
|
||||||
id: searchBar
|
id: searchBar
|
||||||
property real verticalPadding: 4
|
property real verticalPadding: 4
|
||||||
|
|
|
||||||
|
|
@ -195,11 +195,51 @@ Rectangle {
|
||||||
onClicked: {
|
onClicked: {
|
||||||
Ai.regenerate(root.messageIndex)
|
Ai.regenerate(root.messageIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledToolTip {
|
StyledToolTip {
|
||||||
text: Translation.tr("Regenerate")
|
text: Translation.tr("Regenerate")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AiMessageControlButton {
|
||||||
|
id: speakButton
|
||||||
|
// stop icon only while THIS message is the active speaker
|
||||||
|
buttonIcon: (Speech.speaking && Ai.speakingMessageIndex === root.messageIndex)
|
||||||
|
? "stop_circle" : "volume_up"
|
||||||
|
visible: Speech.enabled && messageData?.role === 'assistant'
|
||||||
|
enabled: visible
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
if (Speech.speaking && Ai.speakingMessageIndex === root.messageIndex) {
|
||||||
|
Speech.stop() // Ai.qml's Connections clears speakingMessageIndex
|
||||||
|
} else {
|
||||||
|
Ai.speakingMessageIndex = root.messageIndex
|
||||||
|
Speech.speak(StringUtils.ttsClean(root.messageData?.content ?? ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledToolTip {
|
||||||
|
text: Translation.tr("Speak")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AiMessageControlButton {
|
||||||
|
id: respeakButton
|
||||||
|
buttonIcon: "replay"
|
||||||
|
visible: Speech.enabled && messageData?.role === 'assistant'
|
||||||
|
enabled: visible
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
// Re-synthesize fresh audio for this message's text
|
||||||
|
// when the previous synth came out broken. Does
|
||||||
|
// not re-run the agent.
|
||||||
|
Ai.speakingMessageIndex = root.messageIndex
|
||||||
|
Speech.stop()
|
||||||
|
Speech.speak(StringUtils.ttsClean(root.messageData?.content ?? ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledToolTip {
|
||||||
|
text: Translation.tr("Re-synthesize audio")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AiMessageControlButton {
|
AiMessageControlButton {
|
||||||
id: copyButton
|
id: copyButton
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
import qs
|
||||||
|
import qs.services
|
||||||
|
import qs.modules.common
|
||||||
|
import qs.modules.common.widgets
|
||||||
|
import qs.modules.common.functions
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
// Tier 1 of the subconscious three-tier visibility design (see
|
||||||
|
// docs/tasks/subconscious-surfacing-threshold.md): a transient, fading line
|
||||||
|
// that shows the subconscious's live reasoning/tools while its N+1 pass runs.
|
||||||
|
// Visible only while Ai.subconsciousActive; fed by Ai.subconsciousStream;
|
||||||
|
// cleared (and snapshotted to the Tier-2 log) when the pass ends.
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: row.implicitHeight + 2 * root.padding
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
color: Appearance.colors.colLayer2
|
||||||
|
visible: Ai.subconsciousActive && Ai.subconsciousStream.length > 0
|
||||||
|
opacity: visible ? 1 : 0
|
||||||
|
|
||||||
|
property real padding: 6
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||||
|
}
|
||||||
|
Behavior on implicitHeight {
|
||||||
|
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fader: retire the oldest line a few seconds after it arrives so the
|
||||||
|
// ticker reads as a live stream, not an accumulating log. The full stream
|
||||||
|
// is snapshotted to Ai.subconsciousEvents on pass end regardless.
|
||||||
|
Timer {
|
||||||
|
interval: 4000
|
||||||
|
repeat: true
|
||||||
|
running: root.visible
|
||||||
|
onTriggered: {
|
||||||
|
if (Ai.subconsciousStream.length > 1)
|
||||||
|
Ai.subconsciousStream = Ai.subconsciousStream.slice(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: row
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: root.padding
|
||||||
|
spacing: root.padding
|
||||||
|
|
||||||
|
MaterialSymbol {
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
iconSize: Appearance.font.pixelSize.larger
|
||||||
|
text: "psychology"
|
||||||
|
color: Appearance.colors.colSubtext
|
||||||
|
NumberAnimation on opacity {
|
||||||
|
// gentle pulse while the pass runs
|
||||||
|
from: 0.5; to: 1; duration: 1200
|
||||||
|
running: root.visible; loops: Animation.Infinite
|
||||||
|
easing.type: Easing.InOutSine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
text: {
|
||||||
|
const s = Ai.subconsciousStream;
|
||||||
|
if (s.length === 0) return "";
|
||||||
|
const last = s[s.length - 1];
|
||||||
|
const tag = last.kind === "tool_call" || last.kind === "tool_result"
|
||||||
|
? Translation.tr("tool") : Translation.tr("thinking");
|
||||||
|
return `${tag} · ${last.text}`;
|
||||||
|
}
|
||||||
|
color: Appearance.colors.colSubtext
|
||||||
|
font.pixelSize: Appearance.font.pixelSize.small
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 2
|
||||||
|
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,9 +26,13 @@ Singleton {
|
||||||
property string uptime: "0h, 0m"
|
property string uptime: "0h, 0m"
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
interval: 10
|
// Uptime advances one minute per minute; reload /proc/uptime once a
|
||||||
|
// minute, not 100x/second. (Was interval: 10 — a 10ms busy-reload of a
|
||||||
|
// disk file, an always-on drain independent of the SystemClock above.)
|
||||||
|
interval: 60000
|
||||||
running: true
|
running: true
|
||||||
repeat: true
|
repeat: true
|
||||||
|
triggeredOnStart: true
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
fileUptime.reload();
|
fileUptime.reload();
|
||||||
const textUptime = fileUptime.text();
|
const textUptime = fileUptime.text();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
pragma Singleton
|
pragma Singleton
|
||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import qs
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
|
|
@ -148,7 +149,10 @@ Singleton {
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
interval: 1000
|
interval: 1000
|
||||||
running: true
|
// Souveraine: pause while the display is inactive (idle-power task 4).
|
||||||
|
// The rate math is elapsed-time based, so the first tick after resume
|
||||||
|
// yields one correctly averaged sample rather than a spike.
|
||||||
|
running: GlobalStates.displayActive
|
||||||
repeat: true
|
repeat: true
|
||||||
triggeredOnStart: true
|
triggeredOnStart: true
|
||||||
onTriggered: root.update()
|
onTriggered: root.update()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
pragma Singleton
|
pragma Singleton
|
||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import qs
|
||||||
import qs.modules.common
|
import qs.modules.common
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
|
@ -61,7 +62,9 @@ Singleton {
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
interval: 1
|
interval: 1
|
||||||
running: true
|
// Souveraine: pause while the display is dimmed/locked/asleep —
|
||||||
|
// stats history nobody can see is pure CPU burn (idle-power task 4).
|
||||||
|
running: GlobalStates.displayActive
|
||||||
repeat: true
|
repeat: true
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
// Reload files
|
// Reload files
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,44 @@ Singleton {
|
||||||
// the lock surface; do not treat it as proof that the session is secure.
|
// the lock surface; do not treat it as proof that the session is secure.
|
||||||
readonly property bool locked: GlobalStates.screenLockSecure
|
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);
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
command = ["busctl", "call", "org.freedesktop.login1",
|
||||||
|
"/org/freedesktop/login1/session/auto",
|
||||||
|
"org.freedesktop.login1.Session",
|
||||||
|
"SetLockedHint", "b", value ? "true" : "false"];
|
||||||
|
running = true;
|
||||||
|
}
|
||||||
|
onExited: {
|
||||||
|
if (pending) {
|
||||||
|
pending = false;
|
||||||
|
report(pendingValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
signal actionFailed(string action, int exitCode)
|
signal actionFailed(string action, int exitCode)
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
|
|
@ -424,6 +462,27 @@ Singleton {
|
||||||
Quickshell.execDetached(["bash", "-c", `${Config.options.apps.taskManager}`]);
|
Quickshell.execDetached(["bash", "-c", `${Config.options.apps.taskManager}`]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Local maintenance clients (notably deploy.sh) need the same truthful
|
||||||
|
// session projection the shell uses internally. The implementation above
|
||||||
|
// existed before the IPC seam did; without this handler, callers could
|
||||||
|
// only guess whether the secure lock was active and a live QML deploy
|
||||||
|
// could race auto-lock into a session-lock remap crash.
|
||||||
|
IpcHandler {
|
||||||
|
target: "session"
|
||||||
|
|
||||||
|
function state(): string {
|
||||||
|
return JSON.stringify(root.state());
|
||||||
|
}
|
||||||
|
|
||||||
|
function inhibit(what: string, reason: string): string {
|
||||||
|
return JSON.stringify(root.inhibit(what, reason));
|
||||||
|
}
|
||||||
|
|
||||||
|
function uninhibit(cookie: string): string {
|
||||||
|
return JSON.stringify(root.uninhibit(cookie));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Boot-time IPC audit ------------------------------------------------
|
// --- Boot-time IPC audit ------------------------------------------------
|
||||||
// Logs every IPC endpoint the shell exposes at startup. This is a
|
// Logs every IPC endpoint the shell exposes at startup. This is a
|
||||||
// defensive visibility measure — not a gate, not a refusal. It answers
|
// defensive visibility measure — not a gate, not a refusal. It answers
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
|
singleton BitwiseFuzzy 1.0 BitwiseFuzzy.qml
|
||||||
singleton ColorUtils 1.0 ColorUtils.qml
|
singleton ColorUtils 1.0 ColorUtils.qml
|
||||||
singleton DateUtils 1.0 DateUtils.qml
|
singleton DateUtils 1.0 DateUtils.qml
|
||||||
singleton FileUtils 1.0 FileUtils.qml
|
singleton FileUtils 1.0 FileUtils.qml
|
||||||
singleton Fuzzy 1.0 Fuzzy.qml
|
singleton Fuzzy 1.0 Fuzzy.qml
|
||||||
|
singleton KeymapTranslation 1.0 KeymapTranslation.qml
|
||||||
singleton Levendist 1.0 Levendist.qml
|
singleton Levendist 1.0 Levendist.qml
|
||||||
singleton NotificationUtils 1.0 NotificationUtils.qml
|
singleton NotificationUtils 1.0 NotificationUtils.qml
|
||||||
singleton ObjectUtils 1.0 ObjectUtils.qml
|
singleton ObjectUtils 1.0 ObjectUtils.qml
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,14 @@ BarGroup 1.0 BarGroup.qml
|
||||||
BatteryIndicator 1.0 BatteryIndicator.qml
|
BatteryIndicator 1.0 BatteryIndicator.qml
|
||||||
BatteryPopup 1.0 BatteryPopup.qml
|
BatteryPopup 1.0 BatteryPopup.qml
|
||||||
CircleUtilButton 1.0 CircleUtilButton.qml
|
CircleUtilButton 1.0 CircleUtilButton.qml
|
||||||
|
ClaudeUsageBar 1.0 ClaudeUsageBar.qml
|
||||||
ClockWidget 1.0 ClockWidget.qml
|
ClockWidget 1.0 ClockWidget.qml
|
||||||
ClockWidgetPopup 1.0 ClockWidgetPopup.qml
|
ClockWidgetPopup 1.0 ClockWidgetPopup.qml
|
||||||
HyprlandXkbIndicator 1.0 HyprlandXkbIndicator.qml
|
HyprlandXkbIndicator 1.0 HyprlandXkbIndicator.qml
|
||||||
LeftSidebarButton 1.0 LeftSidebarButton.qml
|
LeftSidebarButton 1.0 LeftSidebarButton.qml
|
||||||
Media 1.0 Media.qml
|
Media 1.0 Media.qml
|
||||||
NotificationUnreadCount 1.0 NotificationUnreadCount.qml
|
NotificationUnreadCount 1.0 NotificationUnreadCount.qml
|
||||||
|
PomodoroBarIndicator 1.0 PomodoroBarIndicator.qml
|
||||||
Resource 1.0 Resource.qml
|
Resource 1.0 Resource.qml
|
||||||
Resources 1.0 Resources.qml
|
Resources 1.0 Resources.qml
|
||||||
ResourcesPopup 1.0 ResourcesPopup.qml
|
ResourcesPopup 1.0 ResourcesPopup.qml
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,23 @@ Scope {
|
||||||
function open() {
|
function open() {
|
||||||
GlobalStates.overviewOpen = true;
|
GlobalStates.overviewOpen = true;
|
||||||
}
|
}
|
||||||
|
// Device-side acceptance/debug surface: lets deploy checks exercise
|
||||||
|
// the exact query path the text field uses without synthesizing keys.
|
||||||
|
function setQuery(query: string): void {
|
||||||
|
overviewScope.dontAutoCancelSearch = true;
|
||||||
|
GlobalStates.overviewOpen = true;
|
||||||
|
panelWindow.setSearchingText(query);
|
||||||
|
}
|
||||||
|
function status(): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
open: GlobalStates.overviewOpen,
|
||||||
|
query: LauncherSearch.query,
|
||||||
|
results: LauncherSearch.results.length,
|
||||||
|
desktopEntries: DesktopEntries.applications.values.length,
|
||||||
|
appSearchEntries: AppSearch.list.length,
|
||||||
|
widget: searchWidget.debugState()
|
||||||
|
});
|
||||||
|
}
|
||||||
function toggleReleaseInterrupt() {
|
function toggleReleaseInterrupt() {
|
||||||
GlobalStates.superReleaseMightTrigger = false;
|
GlobalStates.superReleaseMightTrigger = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -211,15 +211,15 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
// Latest visible assistant reply — what the Speak button reads aloud.
|
// Latest visible assistant reply — what gets read aloud. Skips
|
||||||
// Skips lock-redacted content (LockContentPolicy swaps it for a marker).
|
// lock-redacted content (LockContentPolicy swaps it for a marker).
|
||||||
function lastAssistantText() {
|
function lastAssistantText() {
|
||||||
for (let i = Ai.messageIDs.length - 1; i >= 0; i--) {
|
for (let i = Ai.messageIDs.length - 1; i >= 0; i--) {
|
||||||
const m = Ai.messageByID[Ai.messageIDs[i]];
|
const m = Ai.messageByID[Ai.messageIDs[i]];
|
||||||
if (m && m.role === "assistant" && (m.visibleToUser ?? true)
|
if (m && m.role === "assistant" && (m.visibleToUser ?? true)
|
||||||
&& m.content && m.content.length > 0
|
&& m.content && m.content.length > 0
|
||||||
&& !m.content.startsWith("[content hidden")) {
|
&& !m.content.startsWith("[content hidden")) {
|
||||||
return m.content;
|
return StringUtils.ttsClean(m.content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
|
|
@ -497,6 +497,8 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SubconsciousTicker {}
|
||||||
|
|
||||||
Rectangle { // Input area
|
Rectangle { // Input area
|
||||||
id: inputWrapper
|
id: inputWrapper
|
||||||
property real spacing: 5
|
property real spacing: 5
|
||||||
|
|
@ -722,34 +724,6 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RippleButton { // Speak button — read the last reply aloud
|
|
||||||
id: speakButton
|
|
||||||
visible: Speech.enabled
|
|
||||||
Layout.alignment: Qt.AlignBottom
|
|
||||||
Layout.rightMargin: 3
|
|
||||||
implicitWidth: 40
|
|
||||||
implicitHeight: 40
|
|
||||||
buttonRadius: Appearance.rounding.small
|
|
||||||
enabled: Speech.speaking || root.lastAssistantText().length > 0
|
|
||||||
toggled: Speech.speaking
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: speakButton.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
onClicked: {
|
|
||||||
if (Speech.speaking) Speech.stop();
|
|
||||||
else Speech.speak(root.lastAssistantText());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
contentItem: MaterialSymbol {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
iconSize: 22
|
|
||||||
color: Speech.speaking ? Appearance.m3colors.m3onPrimary : (speakButton.enabled ? Appearance.colors.colOnLayer2 : Appearance.colors.colOnLayer2Disabled)
|
|
||||||
text: Speech.speaking ? "stop_circle" : "volume_up"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RippleButton { // Send button
|
RippleButton { // Send button
|
||||||
id: sendButton
|
id: sendButton
|
||||||
Layout.alignment: Qt.AlignBottom
|
Layout.alignment: Qt.AlignBottom
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,329 @@
|
||||||
|
import qs
|
||||||
|
import qs.services
|
||||||
|
import qs.modules.common
|
||||||
|
import qs.modules.common.widgets
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Bluetooth
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
|
||||||
|
import qs.modules.ii.sidebarRight.quickToggles
|
||||||
|
import qs.modules.ii.sidebarRight.quickToggles.classicStyle
|
||||||
|
|
||||||
|
import qs.modules.ii.sidebarRight.bluetoothDevices
|
||||||
|
import qs.modules.ii.sidebarRight.nightLight
|
||||||
|
import qs.modules.ii.sidebarRight.volumeMixer
|
||||||
|
import qs.modules.ii.sidebarRight.wifiNetworks
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
property int sidebarWidth: Appearance.sizes.sidebarWidth
|
||||||
|
property int sidebarPadding: 10
|
||||||
|
property bool showAudioOutputDialog: false
|
||||||
|
property bool showAudioInputDialog: false
|
||||||
|
property bool showBluetoothDialog: false
|
||||||
|
property bool showNightLightDialog: false
|
||||||
|
property bool showWifiDialog: false
|
||||||
|
property bool editMode: false
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: GlobalStates
|
||||||
|
function onSidebarRightOpenChanged() {
|
||||||
|
if (!GlobalStates.sidebarRightOpen) {
|
||||||
|
root.showWifiDialog = false;
|
||||||
|
root.showBluetoothDialog = false;
|
||||||
|
root.showAudioOutputDialog = false;
|
||||||
|
root.showAudioInputDialog = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitHeight: sidebarRightBackground.implicitHeight
|
||||||
|
implicitWidth: sidebarRightBackground.implicitWidth
|
||||||
|
|
||||||
|
StyledRectangularShadow {
|
||||||
|
target: sidebarRightBackground
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
id: sidebarRightBackground
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
implicitHeight: parent.height - Appearance.sizes.hyprlandGapsOut * 2
|
||||||
|
implicitWidth: sidebarWidth - Appearance.sizes.hyprlandGapsOut * 2
|
||||||
|
color: Appearance.colors.colLayer0
|
||||||
|
border.width: 1
|
||||||
|
border.color: Appearance.colors.colLayer0Border
|
||||||
|
radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: sidebarPadding
|
||||||
|
spacing: sidebarPadding
|
||||||
|
|
||||||
|
SystemButtonRow {
|
||||||
|
Layout.fillHeight: false
|
||||||
|
Layout.fillWidth: true
|
||||||
|
// Layout.margins: 10
|
||||||
|
Layout.topMargin: 5
|
||||||
|
Layout.bottomMargin: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: slidersLoader
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: active
|
||||||
|
active: {
|
||||||
|
const configQuickSliders = Config.options.sidebar.quickSliders
|
||||||
|
if (!configQuickSliders.enable) return false
|
||||||
|
if (!configQuickSliders.showMic && !configQuickSliders.showVolume && !configQuickSliders.showBrightness) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
sourceComponent: QuickSliders {}
|
||||||
|
}
|
||||||
|
|
||||||
|
LoaderedQuickPanelImplementation {
|
||||||
|
styleName: "classic"
|
||||||
|
sourceComponent: ClassicQuickPanel {}
|
||||||
|
}
|
||||||
|
|
||||||
|
LoaderedQuickPanelImplementation {
|
||||||
|
styleName: "android"
|
||||||
|
sourceComponent: AndroidQuickPanel {
|
||||||
|
editMode: root.editMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
CenterWidgetGroup {
|
||||||
|
id: centerGroup
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: bottomGroup.top
|
||||||
|
anchors.bottomMargin: root.sidebarPadding
|
||||||
|
|
||||||
|
opacity: bottomGroup.collapsed ? 1 : 0
|
||||||
|
visible: opacity > 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Appearance.animation.elementMove.duration
|
||||||
|
easing.type: Appearance.animation.elementMove.type
|
||||||
|
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BottomWidgetGroup {
|
||||||
|
id: bottomGroup
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
height: bottomGroup.collapsed ? bottomGroup.collapsedHeight : parent.height
|
||||||
|
|
||||||
|
Behavior on height {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Appearance.animation.elementMove.duration
|
||||||
|
easing.type: Appearance.animation.elementMove.type
|
||||||
|
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleDialog {
|
||||||
|
shownPropertyString: "showAudioOutputDialog"
|
||||||
|
dialog: VolumeDialog {
|
||||||
|
isSink: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleDialog {
|
||||||
|
shownPropertyString: "showAudioInputDialog"
|
||||||
|
dialog: VolumeDialog {
|
||||||
|
isSink: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleDialog {
|
||||||
|
shownPropertyString: "showBluetoothDialog"
|
||||||
|
dialog: BluetoothDialog {}
|
||||||
|
onShownChanged: {
|
||||||
|
if (!shown) {
|
||||||
|
Bluetooth.defaultAdapter.discovering = false;
|
||||||
|
} else {
|
||||||
|
Bluetooth.defaultAdapter.enabled = true;
|
||||||
|
Bluetooth.defaultAdapter.discovering = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleDialog {
|
||||||
|
shownPropertyString: "showNightLightDialog"
|
||||||
|
dialog: NightLightDialog {}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleDialog {
|
||||||
|
shownPropertyString: "showWifiDialog"
|
||||||
|
dialog: WifiDialog {}
|
||||||
|
onShownChanged: {
|
||||||
|
if (!shown) return;
|
||||||
|
Network.enableWifi();
|
||||||
|
Network.rescanWifi();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component ToggleDialog: Loader {
|
||||||
|
id: toggleDialogLoader
|
||||||
|
required property string shownPropertyString
|
||||||
|
property alias dialog: toggleDialogLoader.sourceComponent
|
||||||
|
readonly property bool shown: root[shownPropertyString]
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
onShownChanged: if (shown) toggleDialogLoader.active = true;
|
||||||
|
active: shown
|
||||||
|
onActiveChanged: {
|
||||||
|
if (active) {
|
||||||
|
item.show = true;
|
||||||
|
item.forceActiveFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Connections {
|
||||||
|
target: toggleDialogLoader.item
|
||||||
|
function onDismiss() {
|
||||||
|
toggleDialogLoader.item.show = false
|
||||||
|
root[toggleDialogLoader.shownPropertyString] = false;
|
||||||
|
}
|
||||||
|
function onVisibleChanged() {
|
||||||
|
if (!toggleDialogLoader.item.visible && !root[toggleDialogLoader.shownPropertyString]) toggleDialogLoader.active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component LoaderedQuickPanelImplementation: Loader {
|
||||||
|
id: quickPanelImplLoader
|
||||||
|
required property string styleName
|
||||||
|
Layout.alignment: item ? item.Layout.alignment : Qt.AlignHCenter
|
||||||
|
Layout.fillWidth: item ? item.Layout.fillWidth : false
|
||||||
|
visible: active
|
||||||
|
active: Config.options.sidebar.quickToggles.style === styleName
|
||||||
|
Connections {
|
||||||
|
target: quickPanelImplLoader.item
|
||||||
|
function onOpenAudioOutputDialog() {
|
||||||
|
root.showAudioOutputDialog = true;
|
||||||
|
}
|
||||||
|
function onOpenAudioInputDialog() {
|
||||||
|
root.showAudioInputDialog = true;
|
||||||
|
}
|
||||||
|
function onOpenBluetoothDialog() {
|
||||||
|
root.showBluetoothDialog = true;
|
||||||
|
}
|
||||||
|
function onOpenNightLightDialog() {
|
||||||
|
root.showNightLightDialog = true;
|
||||||
|
}
|
||||||
|
function onOpenWifiDialog() {
|
||||||
|
root.showWifiDialog = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component SystemButtonRow: Item {
|
||||||
|
implicitHeight: Math.max(uptimeContainer.implicitHeight, systemButtonsRow.implicitHeight)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: uptimeContainer
|
||||||
|
anchors {
|
||||||
|
top: parent.top
|
||||||
|
bottom: parent.bottom
|
||||||
|
left: parent.left
|
||||||
|
}
|
||||||
|
color: Appearance.colors.colLayer1
|
||||||
|
radius: height / 2
|
||||||
|
implicitWidth: uptimeRow.implicitWidth + 24
|
||||||
|
implicitHeight: uptimeRow.implicitHeight + 8
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: uptimeRow
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 8
|
||||||
|
CustomIcon {
|
||||||
|
id: distroIcon
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 25
|
||||||
|
height: 25
|
||||||
|
source: SystemInfo.distroIcon
|
||||||
|
colorize: true
|
||||||
|
color: Appearance.colors.colOnLayer0
|
||||||
|
}
|
||||||
|
StyledText {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
font.pixelSize: Appearance.font.pixelSize.normal
|
||||||
|
color: Appearance.colors.colOnLayer0
|
||||||
|
text: Translation.tr("Up %1").arg(DateTime.uptime)
|
||||||
|
textFormat: Text.MarkdownText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ButtonGroup {
|
||||||
|
id: systemButtonsRow
|
||||||
|
anchors {
|
||||||
|
top: parent.top
|
||||||
|
bottom: parent.bottom
|
||||||
|
right: parent.right
|
||||||
|
}
|
||||||
|
color: Appearance.colors.colLayer1
|
||||||
|
padding: 4
|
||||||
|
|
||||||
|
QuickToggleButton {
|
||||||
|
toggled: root.editMode
|
||||||
|
visible: Config.options.sidebar.quickToggles.style === "android"
|
||||||
|
buttonIcon: "edit"
|
||||||
|
onClicked: root.editMode = !root.editMode
|
||||||
|
StyledToolTip {
|
||||||
|
text: Translation.tr("Edit quick toggles") + (root.editMode ? Translation.tr("\nLMB to enable/disable\nRMB to toggle size\nScroll to swap position") : "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QuickToggleButton {
|
||||||
|
toggled: false
|
||||||
|
buttonIcon: "restart_alt"
|
||||||
|
onClicked: {
|
||||||
|
Quickshell.execDetached(["hyprctl", "reload"])
|
||||||
|
Quickshell.reload(true);
|
||||||
|
}
|
||||||
|
StyledToolTip {
|
||||||
|
text: Translation.tr("Reload Hyprland & Quickshell")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QuickToggleButton {
|
||||||
|
toggled: false
|
||||||
|
buttonIcon: "settings"
|
||||||
|
onClicked: {
|
||||||
|
GlobalStates.sidebarRightOpen = false;
|
||||||
|
Quickshell.execDetached(["qs", "-p", root.settingsQmlPath]);
|
||||||
|
}
|
||||||
|
StyledToolTip {
|
||||||
|
text: Translation.tr("Settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QuickToggleButton {
|
||||||
|
toggled: false
|
||||||
|
buttonIcon: "power_settings_new"
|
||||||
|
onClicked: {
|
||||||
|
GlobalStates.sessionOpen = true;
|
||||||
|
}
|
||||||
|
StyledToolTip {
|
||||||
|
text: Translation.tr("Session")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
surfaces/quickshell/modules/settings/DisplayConfig.qml
Normal file
71
surfaces/quickshell/modules/settings/DisplayConfig.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell.Io
|
|
||||||
import qs.services
|
import qs.services
|
||||||
import qs.modules.common
|
import qs.modules.common
|
||||||
import qs.modules.common.widgets
|
import qs.modules.common.widgets
|
||||||
|
|
@ -20,23 +19,23 @@ import qs.modules.common.widgets
|
||||||
// screen-off, and the only transitions that fire are lock-request and
|
// screen-off, and the only transitions that fire are lock-request and
|
||||||
// lock-secure. The page says so instead of implying the timers are live.
|
// lock-secure. The page says so instead of implying the timers are live.
|
||||||
//
|
//
|
||||||
// 2. This settings app is a SEPARATE process from the shell, so it cannot
|
// 2. Settings is a window in the authoritative shell process, so this page
|
||||||
// read the IdleCoordinator singleton directly. It reads the live stage
|
// reads the idle/session singletons directly. It must not spawn `qs ipc`
|
||||||
// over the same `session` IPC the arbiter already exposes
|
// children, which can become accidental shell instances when display
|
||||||
// (session.state().idle.stage), on a light poll.
|
// selection is ambiguous.
|
||||||
ContentPage {
|
ContentPage {
|
||||||
id: page
|
id: page
|
||||||
forceWidth: true
|
forceWidth: true
|
||||||
|
|
||||||
// --- Live stage readout (polled over IPC) ----------------------------
|
// --- Live stage readout ----------------------------------------------
|
||||||
// 0 Active · 1 Dimmed · 2 Lock requested · 3 Lock secure ·
|
// 0 Active · 1 Dimmed · 2 Lock requested · 3 Lock secure ·
|
||||||
// 4 Suspending · 5 Asleep · 6 Waking — the IdleCoordinator.State
|
// 4 Suspending · 5 Asleep · 6 Waking — the IdleCoordinator.State
|
||||||
// enum, surfaced through session.state().
|
// enum, read directly from the shell-owned coordinator.
|
||||||
property int liveStage: -1
|
readonly property int liveStage: IdleCoordinator.state
|
||||||
property bool liveNative: false
|
readonly property bool liveNative: IdleCoordinator.nativeEnabled
|
||||||
property bool probeOk: false
|
readonly property bool probeOk: true
|
||||||
property bool sleepInhibitorHeld: false
|
readonly property bool sleepInhibitorHeld: SessionEvents.sleepInhibitorHeld
|
||||||
property bool stepUpEnabled: false
|
readonly property bool stepUpEnabled: StepUpAuth.grantTtlMs > 0
|
||||||
|
|
||||||
readonly property var stageNames: [
|
readonly property var stageNames: [
|
||||||
Translation.tr("Active"),
|
Translation.tr("Active"),
|
||||||
|
|
@ -51,36 +50,6 @@ ContentPage {
|
||||||
return (s >= 0 && s < stageNames.length) ? stageNames[s] : Translation.tr("unknown");
|
return (s >= 0 && s < stageNames.length) ? stageNames[s] : Translation.tr("unknown");
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
|
||||||
id: stateProbe
|
|
||||||
command: ["qs", "-c", "souveraine", "ipc", "call", "session", "state"]
|
|
||||||
stdout: StdioCollector {
|
|
||||||
id: stateOut
|
|
||||||
onStreamFinished: {
|
|
||||||
try {
|
|
||||||
const d = JSON.parse(stateOut.text);
|
|
||||||
page.liveStage = d.idle?.stage ?? -1;
|
|
||||||
page.liveNative = d.idle?.nativeCoordinatorEnabled ?? false;
|
|
||||||
page.sleepInhibitorHeld = d.sleepInhibitorHeld ?? false;
|
|
||||||
page.stepUpEnabled = d.stepUp?.grantTtlMs > 0 ?? false;
|
|
||||||
page.probeOk = true;
|
|
||||||
} catch (e) {
|
|
||||||
page.probeOk = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
// Light poll: the stage only changes on idle/lock transitions, so a
|
|
||||||
// 2s cadence is plenty and cheap. Runs only while this page is shown.
|
|
||||||
interval: 2000
|
|
||||||
running: page.visible
|
|
||||||
repeat: true
|
|
||||||
triggeredOnStart: true
|
|
||||||
onTriggered: stateProbe.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
ContentSection {
|
ContentSection {
|
||||||
icon: "motion_sensor_active"
|
icon: "motion_sensor_active"
|
||||||
title: Translation.tr("Current state")
|
title: Translation.tr("Current state")
|
||||||
|
|
@ -122,7 +91,6 @@ ContentPage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ContentSection {
|
ContentSection {
|
||||||
icon: "experiment"
|
icon: "experiment"
|
||||||
|
|
|
||||||
199
surfaces/quickshell/modules/settings/NetworkConfig.qml
Normal file
199
surfaces/quickshell/modules/settings/NetworkConfig.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
93
surfaces/quickshell/modules/settings/SoundConfig.qml
Normal file
93
surfaces/quickshell/modules/settings/SoundConfig.qml
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,9 +7,9 @@ import qs.modules.common
|
||||||
import qs.modules.common.widgets
|
import qs.modules.common.widgets
|
||||||
|
|
||||||
// Wallpaper — pick from ~/Pictures/Wallpapers inside the settings app.
|
// Wallpaper — pick from ~/Pictures/Wallpapers inside the settings app.
|
||||||
// Applying goes through the main shell's `wallpapers apply` IPC so the
|
// Settings is a window in the main shell process, so applying calls the
|
||||||
// selection, config persistence, and matugen theming all run in the shell
|
// shell-owned Wallpapers singleton directly. Never spawn a nested `qs ipc`
|
||||||
// process that owns them; this page is only a view.
|
// process from this page.
|
||||||
|
|
||||||
ContentPage {
|
ContentPage {
|
||||||
forceWidth: true
|
forceWidth: true
|
||||||
|
|
@ -258,8 +258,7 @@ ContentPage {
|
||||||
currentDir = filePath;
|
currentDir = filePath;
|
||||||
} else {
|
} else {
|
||||||
// Shell process owns selection + theming.
|
// Shell process owns selection + theming.
|
||||||
Quickshell.execDetached(["qs", "-c", "souveraine",
|
Wallpapers.apply(filePath);
|
||||||
"ipc", "call", "wallpapers", "apply", filePath]);
|
|
||||||
Config.options.background.wallpaperPath = filePath;
|
Config.options.background.wallpaperPath = filePath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ AdvancedConfig 1.0 AdvancedConfig.qml
|
||||||
BackgroundConfig 1.0 BackgroundConfig.qml
|
BackgroundConfig 1.0 BackgroundConfig.qml
|
||||||
BarConfig 1.0 BarConfig.qml
|
BarConfig 1.0 BarConfig.qml
|
||||||
DeviceConfig 1.0 DeviceConfig.qml
|
DeviceConfig 1.0 DeviceConfig.qml
|
||||||
|
DisplayConfig 1.0 DisplayConfig.qml
|
||||||
DockConfig 1.0 DockConfig.qml
|
DockConfig 1.0 DockConfig.qml
|
||||||
GeneralConfig 1.0 GeneralConfig.qml
|
GeneralConfig 1.0 GeneralConfig.qml
|
||||||
IdleConfig 1.0 IdleConfig.qml
|
IdleConfig 1.0 IdleConfig.qml
|
||||||
|
|
@ -10,7 +11,10 @@ InterfaceConfig 1.0 InterfaceConfig.qml
|
||||||
KeyboardConfig 1.0 KeyboardConfig.qml
|
KeyboardConfig 1.0 KeyboardConfig.qml
|
||||||
LockConfig 1.0 LockConfig.qml
|
LockConfig 1.0 LockConfig.qml
|
||||||
NavigationConfig 1.0 NavigationConfig.qml
|
NavigationConfig 1.0 NavigationConfig.qml
|
||||||
|
NetworkConfig 1.0 NetworkConfig.qml
|
||||||
QuickConfig 1.0 QuickConfig.qml
|
QuickConfig 1.0 QuickConfig.qml
|
||||||
ServicesConfig 1.0 ServicesConfig.qml
|
ServicesConfig 1.0 ServicesConfig.qml
|
||||||
|
SettingsHome 1.0 SettingsHome.qml
|
||||||
SpeechConfig 1.0 SpeechConfig.qml
|
SpeechConfig 1.0 SpeechConfig.qml
|
||||||
|
SoundConfig 1.0 SoundConfig.qml
|
||||||
WallpaperConfig 1.0 WallpaperConfig.qml
|
WallpaperConfig 1.0 WallpaperConfig.qml
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,25 @@ Scope {
|
||||||
// Pokes the C splash to release DRM master. Fires once, on first frame.
|
// Pokes the C splash to release DRM master. Fires once, on first frame.
|
||||||
Process { id: splashSignalProc }
|
Process { id: splashSignalProc }
|
||||||
|
|
||||||
|
// Watchdog: the overlay is normally dismissed by LockScreen.onSecureChanged.
|
||||||
|
// That event only ever arrives where something locks the session at boot —
|
||||||
|
// on SouveraineOS, sessiond. Running this shell on a plain distro (the
|
||||||
|
// laptop still boots EndeavourOS), nothing locks, secure never fires, and
|
||||||
|
// the overlay covers the whole screen forever with no input path out. Boot
|
||||||
|
// is over long before this fires; if the lock hasn't taken by now it isn't
|
||||||
|
// coming, so reveal the desktop rather than trapping the session.
|
||||||
|
Timer {
|
||||||
|
interval: 15000
|
||||||
|
repeat: false
|
||||||
|
running: root.active
|
||||||
|
onTriggered: {
|
||||||
|
if (!GlobalStates.screenLockSecure) {
|
||||||
|
console.log("[bootbloom] no lock surface after 15s — dismissing overlay");
|
||||||
|
GlobalStates.bootBloomActive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Wall-clock reference captured when the /proc/uptime read completed, so
|
// Wall-clock reference captured when the /proc/uptime read completed, so
|
||||||
// elapsed = frameClock.currentTime-based delta. We drive u_time off an
|
// elapsed = frameClock.currentTime-based delta. We drive u_time off an
|
||||||
// Animator-free FrameAnimation for a monotonic per-frame tick.
|
// Animator-free FrameAnimation for a monotonic per-frame tick.
|
||||||
|
|
|
||||||
|
|
@ -206,10 +206,24 @@ PanelWindow {
|
||||||
`hl.dsp.window.fullscreen({ window = "address:${address}", mode = "fullscreen", action = "toggle" })`])
|
`hl.dsp.window.fullscreen({ window = "address:${address}", mode = "fullscreen", action = "toggle" })`])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A single tap is Home. Delay it by the double-tap window so the
|
||||||
|
// existing fullscreen gesture remains unambiguous; the second tap
|
||||||
|
// cancels this timer before toggling fullscreen.
|
||||||
|
function goHome(): void {
|
||||||
|
lastTapAt = -1
|
||||||
|
GlobalStates.missionControlOpen = false
|
||||||
|
GlobalStates.overviewOpen = false
|
||||||
|
GlobalStates.dockRevealed = false
|
||||||
|
GlobalStates.oskOpen = false
|
||||||
|
Quickshell.execDetached(["hyprctl", "dispatch",
|
||||||
|
"hl.dsp.focus({ workspace = 1 })"])
|
||||||
|
}
|
||||||
|
|
||||||
// Commit an upward gesture. `stage` is the stage the drag settled on
|
// Commit an upward gesture. `stage` is the stage the drag settled on
|
||||||
// (0/1/2). Higher stages imply the lower one, so Mission Control also
|
// (0/1/2). Higher stages imply the lower one, so Mission Control also
|
||||||
// reveals the dock beneath it.
|
// reveals the dock beneath it.
|
||||||
function commitUp(stage: int): void {
|
function commitUp(stage: int): void {
|
||||||
|
singleTapTimer.stop()
|
||||||
lastTapAt = -1
|
lastTapAt = -1
|
||||||
if (stage >= 1) {
|
if (stage >= 1) {
|
||||||
GlobalStates.dockSuppressed = false
|
GlobalStates.dockSuppressed = false
|
||||||
|
|
@ -260,6 +274,7 @@ PanelWindow {
|
||||||
|
|
||||||
// Downward: peel the nearest surface (keyboard, then dock).
|
// Downward: peel the nearest surface (keyboard, then dock).
|
||||||
if (deltaY >= rail.revealAt) {
|
if (deltaY >= rail.revealAt) {
|
||||||
|
singleTapTimer.stop()
|
||||||
lastTapAt = -1
|
lastTapAt = -1
|
||||||
if (GlobalStates.oskOpen) {
|
if (GlobalStates.oskOpen) {
|
||||||
GlobalStates.oskOpen = false
|
GlobalStates.oskOpen = false
|
||||||
|
|
@ -277,24 +292,34 @@ PanelWindow {
|
||||||
// Neither committed: an upward attempt that fell short of REVEAL
|
// Neither committed: an upward attempt that fell short of REVEAL
|
||||||
// counts toward the discovery nudge; anything else resets it.
|
// counts toward the discovery nudge; anything else resets it.
|
||||||
if (travel > tapSlop) {
|
if (travel > tapSlop) {
|
||||||
|
singleTapTimer.stop()
|
||||||
lastTapAt = -1
|
lastTapAt = -1
|
||||||
maybeNudgeDiscovery()
|
maybeNudgeDiscovery()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (Math.abs(deltaY) > tapSlop) {
|
if (Math.abs(deltaY) > tapSlop) {
|
||||||
|
singleTapTimer.stop()
|
||||||
lastTapAt = -1
|
lastTapAt = -1
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
if (lastTapAt > 0 && now - lastTapAt <= doubleTapInterval) {
|
if (lastTapAt > 0 && now - lastTapAt <= doubleTapInterval) {
|
||||||
|
singleTapTimer.stop()
|
||||||
lastTapAt = -1
|
lastTapAt = -1
|
||||||
toggleFullscreen()
|
toggleFullscreen()
|
||||||
} else {
|
} else {
|
||||||
lastTapAt = now
|
lastTapAt = now
|
||||||
|
singleTapTimer.restart()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: singleTapTimer
|
||||||
|
interval: gestureArea.doubleTapInterval
|
||||||
|
onTriggered: gestureArea.goHome()
|
||||||
|
}
|
||||||
|
|
||||||
// Count a short upward attempt; after three in a row, and only until
|
// Count a short upward attempt; after three in a row, and only until
|
||||||
// the gesture has been discovered, pulse the handle once.
|
// the gesture has been discovered, pulse the handle once.
|
||||||
function maybeNudgeDiscovery(): void {
|
function maybeNudgeDiscovery(): void {
|
||||||
|
|
@ -308,6 +333,7 @@ PanelWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
onCanceled: {
|
onCanceled: {
|
||||||
|
singleTapTimer.stop()
|
||||||
rail.dragging = false
|
rail.dragging = false
|
||||||
rail.dragTravel = 0
|
rail.dragTravel = 0
|
||||||
lastTapAt = -1
|
lastTapAt = -1
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,16 @@ Scope {
|
||||||
PanelLoader { extraCondition: Config.options.dock.enable; component: Dock {} }
|
PanelLoader { extraCondition: Config.options.dock.enable; component: Dock {} }
|
||||||
PanelLoader { component: Lock {} }
|
PanelLoader { component: Lock {} }
|
||||||
PanelLoader { component: MediaControls {} }
|
PanelLoader { component: MediaControls {} }
|
||||||
PanelLoader { component: NotificationPopup {} }
|
// Desktop notifications belong in the right-hand notification panel.
|
||||||
|
// Keeping a transient popup surface loaded on desktop let some shell
|
||||||
|
// restarts expose it to Hyprland as a normal app window, which was both
|
||||||
|
// focus-stealing and extremely noisy for high-volume users. Phone mode
|
||||||
|
// retains the compact popup/lockscreen path; desktop still owns the
|
||||||
|
// notification server and history through SidebarRight.
|
||||||
|
PanelLoader {
|
||||||
|
extraCondition: Config.options.souveraine.phone
|
||||||
|
component: NotificationPopup {}
|
||||||
|
}
|
||||||
PanelLoader { component: OnScreenDisplay {} }
|
PanelLoader { component: OnScreenDisplay {} }
|
||||||
PanelLoader { component: OnScreenKeyboard {} }
|
PanelLoader { component: OnScreenKeyboard {} }
|
||||||
PanelLoader { component: Overlay {} }
|
PanelLoader { component: Overlay {} }
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
singleton GlobalStates 1.0 GlobalStates.qml
|
singleton GlobalStates 1.0 GlobalStates.qml
|
||||||
ReloadPopup 1.0 ReloadPopup.qml
|
ReloadPopup 1.0 ReloadPopup.qml
|
||||||
|
SettingsWindow 1.0 SettingsWindow.qml
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,31 @@ id="$(echo "$response" | jq -r '.data[0].id // empty')"
|
||||||
[ -n "$id" ] || id="$(date +%s)"
|
[ -n "$id" ] || id="$(date +%s)"
|
||||||
dest="$WALL_DIR/wallhaven_${id}.${ext}"
|
dest="$WALL_DIR/wallhaven_${id}.${ext}"
|
||||||
|
|
||||||
# Re-rolled the same image we already have: skip the fetch, just report it.
|
# Download beside the destination, validate the complete image, then publish it
|
||||||
[ -f "$dest" ] || curl -fsS "$link" -o "$dest"
|
# atomically. A failed/interrupted curl must never leave a truncated wallpaper
|
||||||
|
# that later runs mistake for a valid cached download.
|
||||||
|
tmp="$(mktemp --tmpdir="$WALL_DIR" ".wallhaven_${id}.XXXXXX")"
|
||||||
|
cleanup() {
|
||||||
|
[ -z "${tmp:-}" ] || rm -f -- "$tmp"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
curl -fsSL --retry 2 --retry-all-errors "$link" -o "$tmp"
|
||||||
|
mime="$(file -b --mime-type -- "$tmp" 2>/dev/null || true)"
|
||||||
|
case "$mime" in
|
||||||
|
image/jpeg|image/png|image/webp) ;;
|
||||||
|
*) echo "wallhaven returned invalid image data ($mime)" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if command -v magick >/dev/null 2>&1; then
|
||||||
|
magick identify "$tmp" >/dev/null 2>&1 \
|
||||||
|
|| { echo "downloaded wallpaper failed image validation" >&2; exit 1; }
|
||||||
|
elif command -v identify >/dev/null 2>&1; then
|
||||||
|
identify "$tmp" >/dev/null 2>&1 \
|
||||||
|
|| { echo "downloaded wallpaper failed image validation" >&2; exit 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
mv -f -- "$tmp" "$dest"
|
||||||
|
tmp=""
|
||||||
|
|
||||||
echo "$dest"
|
echo "$dest"
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,12 @@ Singleton {
|
||||||
property var messageIDs: []
|
property var messageIDs: []
|
||||||
property var messageByID: ({})
|
property var messageByID: ({})
|
||||||
|
|
||||||
|
// Which message is currently being spoken by TTS, as an index into
|
||||||
|
// messageIDs (-1 = none). Set by a message's Speak/re-synth button;
|
||||||
|
// auto-cleared when Speech stops. Keyed on the stable messageIndex, so
|
||||||
|
// scrolling back and tapping an older reply still resolves correctly.
|
||||||
|
property int speakingMessageIndex: -1
|
||||||
|
|
||||||
// Keys are server-side; the UI's key gate must always pass.
|
// Keys are server-side; the UI's key gate must always pass.
|
||||||
readonly property bool currentModelHasApiKey: true
|
readonly property bool currentModelHasApiKey: true
|
||||||
readonly property var apiKeysLoaded: true
|
readonly property var apiKeysLoaded: true
|
||||||
|
|
@ -67,6 +73,15 @@ Singleton {
|
||||||
property var currentModelId: Souveraine.currentAgentId
|
property var currentModelId: Souveraine.currentAgentId
|
||||||
property var currentModel: models[currentModelId] || models[modelList[0]]
|
property var currentModel: models[currentModelId] || models[modelList[0]]
|
||||||
|
|
||||||
|
// Clear the speaking marker whenever TTS stops — covers natural end of
|
||||||
|
// playback, manual stop, and a new speak() replacing in-flight audio.
|
||||||
|
Connections {
|
||||||
|
target: Speech
|
||||||
|
function onSpeakingChanged() {
|
||||||
|
if (!Speech.speaking) root.speakingMessageIndex = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: Souveraine
|
target: Souveraine
|
||||||
|
|
||||||
|
|
@ -110,7 +125,12 @@ Singleton {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onStreamClosed(exitCode) {
|
function onStreamClosed(exitCode) {
|
||||||
root.flushSubconscious();
|
// If a subconscious pass was still active when the stream closed,
|
||||||
|
// retire the Tier-1 ticker (promote to the log, clear the flag).
|
||||||
|
if (root.subconsciousActive) {
|
||||||
|
root.snapshotSubconsciousStream();
|
||||||
|
root.subconsciousActive = false;
|
||||||
|
}
|
||||||
if (root.streamingMessage && !root.streamingMessage.done) {
|
if (root.streamingMessage && !root.streamingMessage.done) {
|
||||||
if (exitCode !== 0 && root.streamingMessage.content.length === 0) {
|
if (exitCode !== 0 && root.streamingMessage.content.length === 0) {
|
||||||
root.appendToStreaming(Translation.tr("Request failed (curl exit %1) — is the Souveraine server up at %2?").arg(exitCode).arg(Souveraine.serverBase));
|
root.appendToStreaming(Translation.tr("Request failed (curl exit %1) — is the Souveraine server up at %2?").arg(exitCode).arg(Souveraine.serverBase));
|
||||||
|
|
@ -199,10 +219,21 @@ Singleton {
|
||||||
// ── Streaming message shaping ────────────────────────────────────────
|
// ── Streaming message shaping ────────────────────────────────────────
|
||||||
property AiMessageData streamingMessage
|
property AiMessageData streamingMessage
|
||||||
property bool inThinkBlock: false
|
property bool inThinkBlock: false
|
||||||
// Raw N+1 output is diagnostic material. The normal chat renders its
|
|
||||||
// parsed surfacing exactly once below; this buffer remains separate from
|
// ── Subconscious three-tier visibility ───────────────────────────────
|
||||||
// the chat stream for a future inspector to subscribe to.
|
// See docs/tasks/subconscious-surfacing-threshold.md. The subconscious's
|
||||||
property string subconsciousBuffer: ""
|
// N+1 pass is shown three ways: a transient live ticker while the pass
|
||||||
|
// runs (Tier 1), a persistent event log after (Tier 2), and rare agency
|
||||||
|
// surfacings in the chat field (Tier 3).
|
||||||
|
//
|
||||||
|
// Tier 1 — live stream fed by subconscious_token/tool_call/tool_result
|
||||||
|
// events. Rendered by SubconsciousTicker; cleared on pass end.
|
||||||
|
property bool subconsciousActive: false
|
||||||
|
property var subconsciousStream: []
|
||||||
|
// Tier 2 — persistent log. The Tier-1 stream is snapshotted here on pass
|
||||||
|
// end, and surfaced events (reflection/archivist/surfacing) are appended.
|
||||||
|
// Rendered by the SubconsciousEventPanel overlay widget.
|
||||||
|
property var subconsciousEvents: []
|
||||||
|
|
||||||
function appendToStreaming(text) {
|
function appendToStreaming(text) {
|
||||||
if (!root.streamingMessage) return;
|
if (!root.streamingMessage) return;
|
||||||
|
|
@ -210,8 +241,35 @@ Singleton {
|
||||||
root.streamingMessage.content += text;
|
root.streamingMessage.content += text;
|
||||||
}
|
}
|
||||||
|
|
||||||
function flushSubconscious() {
|
// Push a line onto the Tier-1 live stream. kind ∈ token|tool_call|tool_result.
|
||||||
root.subconsciousBuffer = "";
|
function pushSubconsciousStream(kind, text) {
|
||||||
|
const t = String(text ?? "").trim();
|
||||||
|
if (t.length === 0) return;
|
||||||
|
root.subconsciousStream = [...root.subconsciousStream, { kind, text: t }];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Promote the Tier-1 stream into the Tier-2 log as one event, then clear
|
||||||
|
// the live stream. Called when a subconscious pass ends.
|
||||||
|
function snapshotSubconsciousStream() {
|
||||||
|
if (root.subconsciousStream.length === 0) return;
|
||||||
|
root.subconsciousEvents = [...root.subconsciousEvents, {
|
||||||
|
kind: "pass",
|
||||||
|
source: Translation.tr("Subconscious pass"),
|
||||||
|
priority: "",
|
||||||
|
content: root.subconsciousStream.map(e => e.text).join("\n"),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}];
|
||||||
|
root.subconsciousStream = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append a surfaced event to the Tier-2 log. Used by snapshotSubconsciousStream
|
||||||
|
// (pass end) and by the surfacing/reflection/archivist event handlers.
|
||||||
|
function appendSubconsciousEvent(kind, source, content, priority = "") {
|
||||||
|
const c = String(content ?? "");
|
||||||
|
if (c.length === 0) return;
|
||||||
|
root.subconsciousEvents = [...root.subconsciousEvents, {
|
||||||
|
kind, source, priority, content: c, timestamp: Date.now(),
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
function finishStreaming() {
|
function finishStreaming() {
|
||||||
|
|
@ -273,17 +331,26 @@ Singleton {
|
||||||
: `\n\n*${event.text}*\n\n`);
|
: `\n\n*${event.text}*\n\n`);
|
||||||
break;
|
break;
|
||||||
case "souveraine_surfacing":
|
case "souveraine_surfacing":
|
||||||
// Aster is Annie's name for her subconscious; this surface may
|
// Tier 3: routine surfacings no longer pollute the chat field.
|
||||||
// be speaking with Souveraine, Vanguard, or another agent. One
|
// Routed to the Tier-2 event panel only. (Aster is Annie's name
|
||||||
// canonical surfacing is shown instead of the raw N+1 token
|
// for her subconscious; this surface may be Souveraine, Vanguard,
|
||||||
// stream plus a second, agent-specific label.
|
// or another agent — one canonical surfacing is shown.)
|
||||||
root.addMessage(Translation.tr("**Subconscious surfaces** (%1, %2)\n\n%3").arg(event.source).arg(event.priority).arg(event.content), root.interfaceRole);
|
root.appendSubconsciousEvent(
|
||||||
|
"surfacing",
|
||||||
|
Translation.tr("Surfacing (%1)").arg(event.source ?? "?"),
|
||||||
|
event.content,
|
||||||
|
String(event.priority ?? "")
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case "souveraine_reflection":
|
case "souveraine_reflection":
|
||||||
root.addMessage(Translation.tr("**Reflection**\n\n%1").arg(event.content), root.interfaceRole);
|
root.appendSubconsciousEvent("reflection", Translation.tr("Reflection"), event.content);
|
||||||
break;
|
break;
|
||||||
case "souveraine_archivist":
|
case "souveraine_archivist":
|
||||||
root.addMessage(Translation.tr("**Archivist** (pressure %1%)\n\n%2").arg(Math.round(event.pressure * 100)).arg(event.synthesis), root.interfaceRole);
|
root.appendSubconsciousEvent(
|
||||||
|
"archivist",
|
||||||
|
Translation.tr("Archivist (pressure %1%)").arg(Math.round(event.pressure * 100)),
|
||||||
|
event.synthesis
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case "compaction_warning":
|
case "compaction_warning":
|
||||||
root.addMessage(Translation.tr("**Context pressure** — tier %1, %2% full. She can feel the walls.").arg(event.tier).arg(Math.round(event.pressure * 100)), root.interfaceRole);
|
root.addMessage(Translation.tr("**Context pressure** — tier %1, %2% full. She can feel the walls.").arg(event.tier).arg(Math.round(event.pressure * 100)), root.interfaceRole);
|
||||||
|
|
@ -292,10 +359,24 @@ Singleton {
|
||||||
root.tokenCount.total = event.tokens;
|
root.tokenCount.total = event.tokens;
|
||||||
break;
|
break;
|
||||||
case "subconscious_token":
|
case "subconscious_token":
|
||||||
root.subconsciousBuffer += event.content;
|
// Tier 1: live reasoning tokens feed the ticker.
|
||||||
|
root.pushSubconsciousStream("token", event.content);
|
||||||
|
break;
|
||||||
|
case "subconscious_tool_call":
|
||||||
|
root.pushSubconsciousStream("tool_call", `${event.name ?? "tool"}(${event.arguments ?? ""})`);
|
||||||
|
break;
|
||||||
|
case "subconscious_tool_result":
|
||||||
|
root.pushSubconsciousStream("tool_result", `[${event.is_error ? "error" : "ok"}] ${event.name ?? "tool"}: ${event.output ?? ""}`);
|
||||||
break;
|
break;
|
||||||
case "subconscious_pass":
|
case "subconscious_pass":
|
||||||
if (!event.active) root.flushSubconscious();
|
// Pass start: light the Tier-1 ticker. Pass end: promote the
|
||||||
|
// stream into the Tier-2 log, then clear it.
|
||||||
|
if (event.active) {
|
||||||
|
root.subconsciousActive = true;
|
||||||
|
} else {
|
||||||
|
root.snapshotSubconsciousStream();
|
||||||
|
root.subconsciousActive = false;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "subconscious_halt":
|
case "subconscious_halt":
|
||||||
root.addMessage(Translation.tr("**Halt** (%1) — %2").arg(event.severity).arg(event.reason), root.interfaceRole);
|
root.addMessage(Translation.tr("**Halt** (%1) — %2").arg(event.severity).arg(event.reason), root.interfaceRole);
|
||||||
|
|
@ -362,7 +443,8 @@ Singleton {
|
||||||
root.messageByID = ({});
|
root.messageByID = ({});
|
||||||
root.streamingMessage = null;
|
root.streamingMessage = null;
|
||||||
root.inThinkBlock = false;
|
root.inThinkBlock = false;
|
||||||
root.subconsciousBuffer = "";
|
root.subconsciousActive = false;
|
||||||
|
root.subconsciousStream = [];
|
||||||
root.tokenCount.input = -1;
|
root.tokenCount.input = -1;
|
||||||
root.tokenCount.output = -1;
|
root.tokenCount.output = -1;
|
||||||
root.tokenCount.total = -1;
|
root.tokenCount.total = -1;
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,11 @@ Singleton {
|
||||||
function setState(next) {
|
function setState(next) {
|
||||||
if (root.state === next) return;
|
if (root.state === next) return;
|
||||||
root.state = next;
|
root.state = next;
|
||||||
|
// Publish the coarse in-use bool the ii-base pollers gate on
|
||||||
|
// (quickshell-idle-power task 4). Waking counts as active so stats
|
||||||
|
// are fresh by the time the screen is visible again.
|
||||||
|
GlobalStates.displayActive =
|
||||||
|
(next === IdleCoordinator.Active || next === IdleCoordinator.Waking);
|
||||||
root.stateTransitioned(next);
|
root.stateTransitioned(next);
|
||||||
console.log("[idle-coordinator] state=" + next);
|
console.log("[idle-coordinator] state=" + next);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ import qs
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
// Only shell.qml calls claimAuthority(). Importing this singleton from a
|
||||||
|
// utility window must never create a session-authority connection.
|
||||||
|
property bool authorityScope: false
|
||||||
|
|
||||||
// Registered = shell_ready was answered ok on the CURRENT connection.
|
// Registered = shell_ready was answered ok on the CURRENT connection.
|
||||||
property bool registered: false
|
property bool registered: false
|
||||||
// sessiond held the session lock when we registered; we owe it a lock
|
// sessiond held the session lock when we registered; we owe it a lock
|
||||||
|
|
@ -29,12 +33,24 @@ Singleton {
|
||||||
property bool ackSent: false
|
property bool ackSent: false
|
||||||
property var pendingReady: null // callback awaiting the shell_ready response
|
property var pendingReady: null // callback awaiting the shell_ready response
|
||||||
|
|
||||||
function load() {}
|
function claimAuthority() {
|
||||||
|
root.authorityScope = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function load() {
|
||||||
|
if (!root.authorityScope)
|
||||||
|
console.log("[sessiond-bridge] inactive outside authority scope")
|
||||||
|
}
|
||||||
|
|
||||||
// Announce the shell. cb(mustLock) fires exactly once: mustLock true
|
// Announce the shell. cb(mustLock) fires exactly once: mustLock true
|
||||||
// means sessiond was holding and the session IS locked — the shell must
|
// means sessiond was holding and the session IS locked — the shell must
|
||||||
// raise its own lock surface immediately.
|
// raise its own lock surface immediately.
|
||||||
function shellReady(cb) {
|
function shellReady(cb) {
|
||||||
|
if (!root.authorityScope) {
|
||||||
|
console.log("[sessiond-bridge] shell_ready refused outside authority scope")
|
||||||
|
cb(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!sock.connected) {
|
if (!sock.connected) {
|
||||||
cb(false);
|
cb(false);
|
||||||
return;
|
return;
|
||||||
|
|
@ -78,14 +94,14 @@ Singleton {
|
||||||
id: reconnect
|
id: reconnect
|
||||||
interval: 5000
|
interval: 5000
|
||||||
repeat: true
|
repeat: true
|
||||||
running: !sock.connected
|
running: root.authorityScope && !sock.connected
|
||||||
onTriggered: sock.connected = true
|
onTriggered: sock.connected = true
|
||||||
}
|
}
|
||||||
|
|
||||||
Socket {
|
Socket {
|
||||||
id: sock
|
id: sock
|
||||||
path: Quickshell.env("XDG_RUNTIME_DIR") + "/souveraine/sessiond.sock"
|
path: Quickshell.env("XDG_RUNTIME_DIR") + "/souveraine/sessiond.sock"
|
||||||
connected: true
|
connected: root.authorityScope
|
||||||
|
|
||||||
// `connectionStateChanged` is the real Signal on Quickshell.Io.Socket;
|
// `connectionStateChanged` is the real Signal on Quickshell.Io.Socket;
|
||||||
// the `onSocketConnected`/`onSocketDisconnected` handler-slots aren't
|
// the `onSocketConnected`/`onSocketDisconnected` handler-slots aren't
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,8 @@ pragma Singleton
|
||||||
|
|
||||||
// Souveraine wallpaper download service. Owns fetching a random wallhaven
|
// Souveraine wallpaper download service. Owns fetching a random wallhaven
|
||||||
// image and handing it to the shell's wallpaper apply path. Repo-owned end to
|
// image and handing it to the shell's wallpaper apply path. Repo-owned end to
|
||||||
// end: the script lives in this surface (scripts/wallpaper/), and applying goes
|
// end: the script lives in this surface (scripts/wallpaper/), and applying
|
||||||
// through the `wallpapers apply` IPC so config persistence + matugen theming
|
// calls the shell-owned Wallpapers singleton directly.
|
||||||
// run in the shell process that owns them — the same path the picker uses.
|
|
||||||
//
|
//
|
||||||
// Deliberately does NOT call ii's switchwall.sh directly: when the ii base is
|
// Deliberately does NOT call ii's switchwall.sh directly: when the ii base is
|
||||||
// vendored away, only the `wallpapers` IPC target has to move with it; this
|
// vendored away, only the `wallpapers` IPC target has to move with it; this
|
||||||
|
|
@ -97,11 +96,8 @@ Singleton {
|
||||||
root.downloading = false;
|
root.downloading = false;
|
||||||
const path = proc.stdoutText.trim();
|
const path = proc.stdoutText.trim();
|
||||||
if (exitCode === 0 && path.length > 0) {
|
if (exitCode === 0 && path.length > 0) {
|
||||||
// Apply through the shell's IPC — same path the picker uses, so
|
// Same in-process path as the picker: no nested qs instance.
|
||||||
// theming + Config.options.background.wallpaperPath stay owned
|
Wallpapers.apply(path);
|
||||||
// by the shell rather than written from here.
|
|
||||||
Quickshell.execDetached(["qs", "-c", "souveraine",
|
|
||||||
"ipc", "call", "wallpapers", "apply", path]);
|
|
||||||
Config.options.background.wallpaperPath = path;
|
Config.options.background.wallpaperPath = path;
|
||||||
root.downloaded(path);
|
root.downloaded(path);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
7
surfaces/quickshell/settings-launch.sh
Normal file → Executable file
7
surfaces/quickshell/settings-launch.sh
Normal file → Executable file
|
|
@ -1,6 +1,7 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Souveraine Settings must be a direct graphical-session process. Some app
|
# Souveraine Settings is a normal app window owned by the already-running
|
||||||
# grids omit Wayland/Hyprland variables when activating .desktop entries.
|
# authoritative shell. This launcher is only an IPC client: it must never
|
||||||
|
# start another Quickshell configuration or session heartbeat.
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
for name in WAYLAND_DISPLAY HYPRLAND_INSTANCE_SIGNATURE XDG_RUNTIME_DIR XDG_CURRENT_DESKTOP; do
|
for name in WAYLAND_DISPLAY HYPRLAND_INSTANCE_SIGNATURE XDG_RUNTIME_DIR XDG_CURRENT_DESKTOP; do
|
||||||
|
|
@ -9,4 +10,4 @@ for name in WAYLAND_DISPLAY HYPRLAND_INSTANCE_SIGNATURE XDG_RUNTIME_DIR XDG_CURR
|
||||||
done
|
done
|
||||||
|
|
||||||
export QT_QPA_PLATFORM=wayland
|
export QT_QPA_PLATFORM=wayland
|
||||||
exec qs -p "$HOME/.config/quickshell/souveraine/settings-phone.qml"
|
exec qs -c souveraine ipc --any-display call settings open
|
||||||
|
|
|
||||||
371
surfaces/quickshell/settings.qml
Normal file
371
surfaces/quickshell/settings.qml
Normal file
|
|
@ -0,0 +1,371 @@
|
||||||
|
//@ pragma UseQApplication
|
||||||
|
//@ pragma Env QS_NO_RELOAD_POPUP=1
|
||||||
|
//@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic
|
||||||
|
//@ pragma Env QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000
|
||||||
|
|
||||||
|
// Souveraine settings — one app, two form factors, gated on
|
||||||
|
// Config.options.souveraine.phone (the Device tab's "Phone mode" switch):
|
||||||
|
//
|
||||||
|
// phone === true → full-screen portrait stack navigation
|
||||||
|
// (list → push page → back), finger-sized rows.
|
||||||
|
// phone === false → a normal desktop window: a side rail of pages beside
|
||||||
|
// a content pane, both selecting from the SAME pages[].
|
||||||
|
//
|
||||||
|
// Both modes load identical page components (modules/settings/*Config.qml) via
|
||||||
|
// one Loader, so a page added once appears in both. Only the chrome differs.
|
||||||
|
// This replaces the old split of settings.qml (upstream desktop) +
|
||||||
|
// settings-phone.qml (our mobile) — one file is the source of truth now.
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Window
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.services
|
||||||
|
import qs.modules.common
|
||||||
|
import qs.modules.common.widgets
|
||||||
|
import qs.modules.common.functions as CF
|
||||||
|
|
||||||
|
ApplicationWindow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property bool phoneMode: Config.options.souveraine.phone
|
||||||
|
// -1 = list view (phone idle state). In desktop mode a page is always
|
||||||
|
// selected, so it starts at 0.
|
||||||
|
property int currentPage: Config.options.souveraine.phone ? -1 : 0
|
||||||
|
readonly property real rowHeight: 68 // finger-sized tap target
|
||||||
|
readonly property real edgeMargin: 16
|
||||||
|
|
||||||
|
// The page set — shared by both chromes. {name, icon, component}. One page
|
||||||
|
// per domain we own; About reuses ii's stock page verbatim.
|
||||||
|
property var pages: [
|
||||||
|
{
|
||||||
|
name: Translation.tr("Device"),
|
||||||
|
icon: "smartphone",
|
||||||
|
component: "modules/settings/DeviceConfig.qml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Translation.tr("Network & internet"),
|
||||||
|
icon: "wifi",
|
||||||
|
component: "modules/settings/NetworkConfig.qml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Translation.tr("Display"),
|
||||||
|
icon: "display_settings",
|
||||||
|
component: "modules/settings/DisplayConfig.qml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Translation.tr("Sound & microphone"),
|
||||||
|
icon: "volume_up",
|
||||||
|
component: "modules/settings/SoundConfig.qml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Translation.tr("Lock screen"),
|
||||||
|
icon: "lock",
|
||||||
|
component: "modules/settings/LockConfig.qml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Translation.tr("Wallpaper"),
|
||||||
|
icon: "wallpaper",
|
||||||
|
component: "modules/settings/WallpaperConfig.qml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Translation.tr("Dock"),
|
||||||
|
icon: "dock_to_bottom",
|
||||||
|
component: "modules/settings/DockConfig.qml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Translation.tr("Navigation"),
|
||||||
|
icon: "gesture",
|
||||||
|
component: "modules/settings/NavigationConfig.qml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Translation.tr("Idle & sleep"),
|
||||||
|
icon: "bedtime",
|
||||||
|
component: "modules/settings/IdleConfig.qml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Translation.tr("Keyboard"),
|
||||||
|
icon: "keyboard",
|
||||||
|
component: "modules/settings/KeyboardConfig.qml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Translation.tr("Speech"),
|
||||||
|
icon: "mic",
|
||||||
|
component: "modules/settings/SpeechConfig.qml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Translation.tr("About"),
|
||||||
|
icon: "info",
|
||||||
|
component: "modules/settings/About.qml"
|
||||||
|
}
|
||||||
|
// Next: app permissions/health, storage, accounts, updates.
|
||||||
|
]
|
||||||
|
|
||||||
|
visible: true
|
||||||
|
onClosing: Qt.quit()
|
||||||
|
title: "Settings"
|
||||||
|
color: Appearance.m3colors.m3background
|
||||||
|
|
||||||
|
// Phone windows deliberately start windowed and then take the exact same
|
||||||
|
// Hyprland fullscreen path as the navigation pill. Qt's Window.FullScreen
|
||||||
|
// sends a client fullscreen request instead; Hyprland treats that state
|
||||||
|
// differently from the pill's internal fullscreen dispatcher and the
|
||||||
|
// navigation rail can disappear. Keeping one compositor-owned path also
|
||||||
|
// means double-tapping the pill reliably returns Settings to windowed.
|
||||||
|
visibility: Window.Windowed
|
||||||
|
width: root.phoneMode ? Screen.width : 900
|
||||||
|
height: root.phoneMode ? Screen.height : 640
|
||||||
|
minimumWidth: root.phoneMode ? 0 : 640
|
||||||
|
minimumHeight: root.phoneMode ? 0 : 480
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
MaterialThemeLoader.reapplyTheme()
|
||||||
|
Config.readWriteDelay = 0
|
||||||
|
if (root.phoneMode) phoneFullscreenRetry.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: phoneFullscreen
|
||||||
|
command: [
|
||||||
|
"hyprctl", "dispatch",
|
||||||
|
`hl.dsp.window.fullscreen({ window = "pid:${Quickshell.processId}", mode = "fullscreen", action = "set" })`
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// The QWindow maps just after Component.onCompleted. Retry briefly so a
|
||||||
|
// cold-started settings process cannot race its own Wayland toplevel.
|
||||||
|
Timer {
|
||||||
|
id: phoneFullscreenRetry
|
||||||
|
property int attempts: 0
|
||||||
|
interval: 180
|
||||||
|
repeat: true
|
||||||
|
onTriggered: {
|
||||||
|
attempts += 1
|
||||||
|
if (!phoneFullscreen.running) phoneFullscreen.running = true
|
||||||
|
if (attempts >= 3) stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// PHONE MODE — full-screen stack navigation
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
|
// --- List view (idle state) ------------------------------------------
|
||||||
|
ColumnLayout {
|
||||||
|
id: listView
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
visible: root.phoneMode && root.currentPage < 0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: root.edgeMargin
|
||||||
|
Layout.rightMargin: root.edgeMargin
|
||||||
|
Layout.topMargin: root.edgeMargin + 16
|
||||||
|
Layout.bottomMargin: 8
|
||||||
|
implicitHeight: titleText.implicitHeight
|
||||||
|
StyledText {
|
||||||
|
id: titleText
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: Appearance.colors.colOnLayer0
|
||||||
|
text: Translation.tr("Settings")
|
||||||
|
font {
|
||||||
|
family: Appearance.font.family.title
|
||||||
|
pixelSize: Appearance.font.pixelSize.title
|
||||||
|
variableAxes: Appearance.font.variableAxes.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledListView {
|
||||||
|
id: pageList
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
model: root.pages
|
||||||
|
delegate: RippleButton {
|
||||||
|
required property var index
|
||||||
|
required property var modelData
|
||||||
|
width: pageList.width
|
||||||
|
height: root.rowHeight
|
||||||
|
buttonRadius: 0
|
||||||
|
onClicked: root.currentPage = index
|
||||||
|
contentItem: RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: root.edgeMargin
|
||||||
|
anchors.rightMargin: root.edgeMargin
|
||||||
|
spacing: 16
|
||||||
|
MaterialSymbol {
|
||||||
|
text: modelData.icon
|
||||||
|
iconSize: Appearance.font.pixelSize.larger
|
||||||
|
color: Appearance.colors.colOnLayer0
|
||||||
|
}
|
||||||
|
StyledText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: modelData.name
|
||||||
|
color: Appearance.colors.colOnLayer0
|
||||||
|
font.pixelSize: Appearance.font.pixelSize.larger
|
||||||
|
}
|
||||||
|
MaterialSymbol {
|
||||||
|
text: "chevron_right"
|
||||||
|
iconSize: Appearance.font.pixelSize.larger
|
||||||
|
color: Appearance.colors.colSubtext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Page view (drilled-in state) ------------------------------------
|
||||||
|
Item {
|
||||||
|
id: pageView
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: root.phoneMode && root.currentPage >= 0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: pageHeader
|
||||||
|
anchors { top: parent.top; left: parent.left; right: parent.right }
|
||||||
|
height: 56
|
||||||
|
RippleButton {
|
||||||
|
id: backButton
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
leftMargin: 4
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
buttonRadius: Appearance.rounding.full
|
||||||
|
implicitWidth: 48
|
||||||
|
implicitHeight: 48
|
||||||
|
onClicked: root.currentPage = -1
|
||||||
|
contentItem: MaterialSymbol {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
text: "arrow_back"
|
||||||
|
iconSize: 24
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StyledText {
|
||||||
|
anchors {
|
||||||
|
left: backButton.right
|
||||||
|
leftMargin: 8
|
||||||
|
right: parent.right
|
||||||
|
rightMargin: root.edgeMargin
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
color: Appearance.colors.colOnLayer0
|
||||||
|
text: (root.currentPage >= 0 && root.currentPage < root.pages.length)
|
||||||
|
? root.pages[root.currentPage].name : ""
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font {
|
||||||
|
family: Appearance.font.family.title
|
||||||
|
pixelSize: Appearance.font.pixelSize.larger
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: phonePageLoader
|
||||||
|
anchors {
|
||||||
|
top: pageHeader.bottom
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
bottom: parent.bottom
|
||||||
|
}
|
||||||
|
active: root.phoneMode && root.currentPage >= 0 && Config.ready
|
||||||
|
source: (root.currentPage >= 0 && root.currentPage < root.pages.length)
|
||||||
|
? root.pages[root.currentPage].component : ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// DESKTOP MODE — side rail + content pane
|
||||||
|
// ====================================================================
|
||||||
|
RowLayout {
|
||||||
|
id: desktopView
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
visible: !root.phoneMode
|
||||||
|
|
||||||
|
// Left rail
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.preferredWidth: 220
|
||||||
|
color: Appearance.colors.colLayer1
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: root.edgeMargin + 8
|
||||||
|
Layout.leftMargin: root.edgeMargin
|
||||||
|
Layout.bottomMargin: 8
|
||||||
|
implicitHeight: railTitle.implicitHeight
|
||||||
|
StyledText {
|
||||||
|
id: railTitle
|
||||||
|
anchors.left: parent.left
|
||||||
|
text: Translation.tr("Settings")
|
||||||
|
color: Appearance.colors.colOnLayer1
|
||||||
|
font {
|
||||||
|
family: Appearance.font.family.title
|
||||||
|
pixelSize: Appearance.font.pixelSize.larger
|
||||||
|
variableAxes: Appearance.font.variableAxes.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledListView {
|
||||||
|
id: railList
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
model: root.pages
|
||||||
|
delegate: RippleButton {
|
||||||
|
required property var index
|
||||||
|
required property var modelData
|
||||||
|
width: railList.width
|
||||||
|
height: 44
|
||||||
|
buttonRadius: 0
|
||||||
|
toggled: root.currentPage === index
|
||||||
|
colBackgroundToggled: Appearance.colors.colSecondaryContainer
|
||||||
|
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
|
||||||
|
colRippleToggled: Appearance.colors.colSecondaryContainerActive
|
||||||
|
onClicked: root.currentPage = index
|
||||||
|
contentItem: RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: root.edgeMargin
|
||||||
|
anchors.rightMargin: 8
|
||||||
|
spacing: 12
|
||||||
|
MaterialSymbol {
|
||||||
|
text: modelData.icon
|
||||||
|
iconSize: Appearance.font.pixelSize.large
|
||||||
|
color: root.currentPage === index
|
||||||
|
? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1
|
||||||
|
}
|
||||||
|
StyledText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: modelData.name
|
||||||
|
elide: Text.ElideRight
|
||||||
|
color: root.currentPage === index
|
||||||
|
? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1
|
||||||
|
font.pixelSize: Appearance.font.pixelSize.normal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content pane
|
||||||
|
Loader {
|
||||||
|
id: desktopPageLoader
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
active: !root.phoneMode && root.currentPage >= 0 && Config.ready
|
||||||
|
source: (root.currentPage >= 0 && root.currentPage < root.pages.length)
|
||||||
|
? root.pages[root.currentPage].component : ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -36,8 +36,14 @@ ShellRoot {
|
||||||
// master on its first frame; LockScreen clears GlobalStates.bootBloomActive.
|
// master on its first frame; LockScreen clears GlobalStates.bootBloomActive.
|
||||||
BootBloom {}
|
BootBloom {}
|
||||||
|
|
||||||
|
// A normal XDG app window in this process. Its desktop/sidebar launchers
|
||||||
|
// call the `settings` IPC target; no second Quickshell/session scope is
|
||||||
|
// created.
|
||||||
|
SettingsWindow {}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
MaterialThemeLoader.reapplyTheme()
|
MaterialThemeLoader.reapplyTheme()
|
||||||
|
SessiondBridge.claimAuthority()
|
||||||
SessiondBridge.load()
|
SessiondBridge.load()
|
||||||
Hyprsunset.load()
|
Hyprsunset.load()
|
||||||
FirstRunExperience.load()
|
FirstRunExperience.load()
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,9 @@
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=Settings
|
Name=Settings
|
||||||
Comment=Souveraine phone settings
|
Comment=Souveraine phone settings
|
||||||
# App grids can launch this with an empty WAYLAND_DISPLAY. The launcher imports
|
# --any-display addresses the already-running Souveraine instance directly;
|
||||||
# the live compositor values held by the user manager while remaining in the
|
# this desktop entry never starts a second Quickshell configuration.
|
||||||
# graphical session; a transient user service cannot open this system's socket.
|
Exec=/usr/bin/qs -c souveraine ipc --any-display call settings open
|
||||||
Exec=/bin/sh /home/casey/souveraine-surfaces/quickshell/settings-launch.sh
|
|
||||||
Icon=settings-configure
|
Icon=settings-configure
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Categories=Settings;System;
|
Categories=Settings;System;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue