Watch
1
0
Fork
You've already forked souveraine
0

settings: wallpaper download + purity toggle, repo-owned

Adds a Download-random-wallpaper button and a 3-way content filter
(SFW/Sketchy/NSFW) to our Wallpaper settings page, backed by a new
WallpaperDownload service and its own download script under
surfaces/quickshell/scripts/wallpaper/ — not ii's scripts/colors tree,
which is going away. Only the 'wallpapers apply' IPC target still comes
from ii and moves with it when the base is vendored.

Downloads are named by wallhaven id so the library accumulates instead of
overwriting a single wallhaven_wallpaper.<ext>. Purity is read from and
written to background.wallhaven.purity in config.json (not in the upstream
Config.options schema). Anchors the root scripts/ gitignore to / so shipped
surface scripts are tracked.
This commit is contained in:
Fimeg 2026-07-21 17:30:32 -04:00
commit cd2150542e
6 changed files with 327 additions and 11 deletions

12
.gitignore vendored
View file

@ -67,8 +67,10 @@ AD_continuation_prompt.md
COMMIT_MESSAGES.txt
vanguard-souveraine-notes.md
# Local-only project scaffolding — kept on disk, never committed to any remote
.superpowers/
journal/
scripts/
website/
# Local-only project scaffolding — kept on disk, never committed to any remote.
# Anchored to repo root (leading /) so shipped surface scripts — e.g.
# surfaces/quickshell/scripts/ — are still tracked.
/.superpowers/
/journal/
/scripts/
/website/

View file

@ -4,10 +4,15 @@
# Model: ~/.config/quickshell/souveraine is BUILT by this script —
# - our files (this repo) are symlinked in, repo stays source of truth
# - untouched upstream directories are borrowed as whole-dir symlinks
# into the pristine ii tree (so upstream updates flow through)
# into the ii tree (so upstream updates flow through)
# - directories where we override any file are composed file-by-file
# ii's own tree is left pristine: no overlay symlinks, no .upstream backups.
# Cutting the cord from ii later = vendor a borrowed dir, per directory.
# The ii tree itself is DEPLOYED from this repo (ii-base/, the pinned base —
# vendored 2026-07-21 after laptop/phone drifted ~900 files): every run
# rsyncs ii-base -> ~/.config/quickshell/ii, so "borrowed from ii" means
# borrowed from the same pin on every device. On the phone (aarch64) the
# ii-phone/ overlay is applied on top — the declared home for phone-only
# files (Cellular, mobile bar/OSK/wallpaper behavior). Never hand-edit
# ~/.config/quickshell/ii; change ii-base/ (or ii-phone/) and redeploy.
#
# deploy.sh compose ~/.config/quickshell/souveraine
# deploy.sh -u remove the souveraine config dir (ii untouched)
@ -25,6 +30,7 @@ SV="${QS}/souveraine"
MANIFEST="
shell.qml souveraine/shell.qml
GlobalStates.qml souveraine/GlobalStates.qml
settings.qml souveraine/settings.qml
settings-phone.qml souveraine/settings-phone.qml
panelFamilies/SouveraineFamily.qml souveraine/panelFamilies/SouveraineFamily.qml
services/Souveraine.qml souveraine/services/Souveraine.qml
@ -35,6 +41,8 @@ services/Idle.qml souveraine/services/Idle.qml
services/IdleCoordinator.qml souveraine/services/IdleCoordinator.qml
services/LockContentPolicy.qml souveraine/services/LockContentPolicy.qml
services/WallpaperAssets.qml souveraine/services/WallpaperAssets.qml
services/WallpaperDownload.qml souveraine/services/WallpaperDownload.qml
scripts/wallpaper/download_wallhaven.sh souveraine/scripts/wallpaper/download_wallhaven.sh
services/ConflictKiller.qml souveraine/services/ConflictKiller.qml
services/SessionEvents.qml souveraine/services/SessionEvents.qml
services/SessiondBridge.qml souveraine/services/SessiondBridge.qml
@ -110,7 +118,8 @@ modules/ii/sidebarRight/volumeMixer/qmldir souveraine/modules/ii/sidebarRight/vo
modules/settings/qmldir souveraine/modules/settings/qmldir
"
PHONE_USB=casey@172.16.42.1
# wifi fallback: PHONE_HOST=casey@10.10.20.234 ./deploy.sh --phone
PHONE_USB="${PHONE_HOST:-casey@172.16.42.1}"
PHONE_DEST="souveraine-surfaces/quickshell"
manifest_lines() { printf '%s\n' "$MANIFEST" | sed '/^[[:space:]]*$/d'; }
@ -175,7 +184,16 @@ if [[ "${1:-}" == "--legacy-clean" ]]; then
exit 0
fi
[[ -d "$II" ]] || { echo "illogical-impulse config not found at $II" >&2; exit 1; }
# --- Sync the pinned ii base --------------------------------------------
# --exclude .git: the pin carries no VCS dirs, and any nested .git already
# on a device (e.g. widgets/shapes) is left alone rather than deleted.
rsync -a --delete --exclude='.git' "$SRC/ii-base/" "$II/"
if [[ "$(uname -m)" == "aarch64" ]]; then
rsync -a "$SRC/ii-phone/" "$II/"
echo "ii synced from pin + phone overlay"
else
echo "ii synced from pin"
fi
# --- Compose -------------------------------------------------------------
# Targets under souveraine/, relative to $SV

View file

@ -36,6 +36,103 @@ ContentPage {
elide: Text.ElideMiddle
}
// Content filter which wallhaven categories the random pick may
// include. Persisted to config.json where the download script reads it.
StyledText {
Layout.fillWidth: true
text: Translation.tr("Allowed content")
color: Appearance.colors.colOnLayer1
font.pixelSize: Appearance.font.pixelSize.small
}
Flow {
Layout.fillWidth: true
spacing: 6
Repeater {
model: [
{ key: "sfw", label: Translation.tr("SFW") },
{ key: "sketchy", label: Translation.tr("Sketchy") },
{ key: "nsfw", label: Translation.tr("NSFW") }
]
RippleButton {
id: purityChip
required property var modelData
property bool on: modelData.key === "sfw" ? WallpaperDownload.puritySfw
: modelData.key === "sketchy" ? WallpaperDownload.puritySketchy
: WallpaperDownload.purityNsfw
padding: 8
buttonRadius: Appearance.rounding.small
toggled: on
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
onClicked: {
if (modelData.key === "sfw")
WallpaperDownload.puritySfw = !WallpaperDownload.puritySfw;
else if (modelData.key === "sketchy")
WallpaperDownload.puritySketchy = !WallpaperDownload.puritySketchy;
else
WallpaperDownload.purityNsfw = !WallpaperDownload.purityNsfw;
WallpaperDownload.savePurity();
}
contentItem: RowLayout {
spacing: 4
MaterialSymbol {
iconSize: 16
text: purityChip.on ? "check" : "add"
color: purityChip.on ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1
}
StyledText {
font.pixelSize: Appearance.font.pixelSize.small
text: modelData.label
color: purityChip.on ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1
}
}
}
}
}
// Download a fresh random wallpaper from wallhaven. Downloads land in
// ~/Pictures/Wallpapers (each a distinct wallhaven_<id> file), then
// apply through the shell like any picked image.
RippleButton {
Layout.fillWidth: true
padding: 10
buttonRadius: Appearance.rounding.small
enabled: !WallpaperDownload.downloading
onClicked: WallpaperDownload.download()
contentItem: RowLayout {
spacing: 8
MaterialSymbol {
iconSize: 20
text: WallpaperDownload.downloading ? "hourglass_top" : "cloud_download"
color: Appearance.colors.colOnLayer1
}
StyledText {
Layout.fillWidth: true
font.pixelSize: Appearance.font.pixelSize.normal
text: WallpaperDownload.downloading
? Translation.tr("Downloading…")
: Translation.tr("Download random wallpaper")
color: Appearance.colors.colOnLayer1
}
}
}
StyledText {
visible: WallpaperDownload.lastError.length > 0
Layout.fillWidth: true
text: WallpaperDownload.lastError
color: Appearance.colors.colError
font.pixelSize: Appearance.font.pixelSize.smaller
wrapMode: Text.WordWrap
}
// Quick directory buttons
Flow {
Layout.fillWidth: true

View file

@ -0,0 +1,80 @@
#!/usr/bin/env bash
# Souveraine wallpaper download — fetch one random wallhaven image into the
# user's Wallpapers folder and print its path. Owned by this repo (not ii's
# scripts/colors/random tree, which is going away): self-contained, no
# QUICKSHELL_CONFIG_NAME assumption, no direct switchwall.sh call. The caller
# (WallpaperDownload service) applies the printed path through the shell's
# `wallpapers apply` IPC so config persistence + matugen theming stay in the
# shell process that owns them.
#
# Usage: download_wallhaven.sh [purity] purity: sfw|sketchy|nsfw|all|csv
# stdout: the downloaded file's absolute path (only on success)
# stderr: human-readable errors; exit non-zero on failure.
set -euo pipefail
pictures_dir() {
if command -v xdg-user-dir >/dev/null 2>&1; then
xdg-user-dir PICTURES
return
fi
local cfg="${XDG_CONFIG_HOME:-$HOME/.config}/user-dirs.dirs"
if [ -f "$cfg" ]; then
# shellcheck disable=SC1090
( . "$cfg" >/dev/null 2>&1; echo "${XDG_PICTURES_DIR/#\$HOME/$HOME}" )
return
fi
echo "$HOME/Pictures"
}
WALL_DIR="$(pictures_dir)/Wallpapers"
mkdir -p "$WALL_DIR"
# API key + purity default: read from illogical-impulse config if present, but
# don't require it. When we vendor our own config store this path is the only
# ii coupling left and moves with it.
ii_config="$HOME/.config/illogical-impulse/config.json"
api_key=""
default_purity="sfw,sketchy"
if [ -f "$ii_config" ] && command -v jq >/dev/null 2>&1; then
api_key="$(jq -r '.background.wallhaven.apiKey // empty' "$ii_config" 2>/dev/null || true)"
p="$(jq -r '.background.wallhaven.purity // empty' "$ii_config" 2>/dev/null || true)"
[ -n "$p" ] && default_purity="$p"
fi
purity="${1:-$default_purity}"
case "$purity" in
sfw) bits="100" ;;
sketchy) bits="010" ;;
nsfw) bits="001" ;;
sfw,sketchy|sketchy,sfw) bits="110" ;;
sfw,nsfw|nsfw,sfw) bits="101" ;;
sketchy,nsfw|nsfw,sketchy) bits="011" ;;
all) bits="111" ;;
*) bits="100" ;;
esac
# 9x16 portrait, at least 1080x2160, random order, one result.
url="https://wallhaven.cc/api/v1/search?ratios=9x16&purity=${bits}&sorting=random&atleast=1080x2160&limit=1"
[ -n "$api_key" ] && url="${url}&apikey=${api_key}"
response="$(curl -fsS "$url")" || { echo "wallhaven request failed" >&2; exit 1; }
if echo "$response" | jq -e '.error' >/dev/null 2>&1; then
echo "wallhaven error: $(echo "$response" | jq -r '.error')" >&2
exit 1
fi
link="$(echo "$response" | jq -r '.data[0].path // empty')"
[ -n "$link" ] || { echo "no wallpapers matched" >&2; exit 1; }
ext="${link##*.}"
# Name by wallhaven ID so every download is a distinct, traceable file and the
# library grows instead of overwriting a single wallhaven_wallpaper.<ext>.
id="$(echo "$response" | jq -r '.data[0].id // empty')"
[ -n "$id" ] || id="$(date +%s)"
dest="$WALL_DIR/wallhaven_${id}.${ext}"
# Re-rolled the same image we already have: skip the fetch, just report it.
[ -f "$dest" ] || curl -fsS "$link" -o "$dest"
echo "$dest"

View file

@ -0,0 +1,114 @@
pragma Singleton
// Souveraine wallpaper download service. Owns fetching a random wallhaven
// 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
// through the `wallpapers apply` IPC so config persistence + matugen theming
// 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
// vendored away, only the `wallpapers` IPC target has to move with it; this
// service and its script are already ours.
import qs.modules.common
import qs.modules.common.functions
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
// busy while a download is in flight bind a button's enabled/spinner to it.
property bool downloading: false
// last error string for the UI (empty = ok).
property string lastError: ""
// Purity flags which wallhaven categories the random pick may include.
// Sourced from ~/.config/illogical-impulse/config.json (background.wallhaven
// .purity, a CSV of sfw/sketchy/nsfw), the same place the download script
// reads. Not in the Config.options schema upstream ships, so we read/write
// the JSON directly rather than binding a schema key that doesn't exist.
property bool puritySfw: true
property bool puritySketchy: false
property bool purityNsfw: false
readonly property string configPath:
FileUtils.trimFileProtocol(Quickshell.env("HOME") + "/.config/illogical-impulse/config.json")
// The CSV the script's [purity] arg expects, or "sfw" if nothing is on
// (never fetch an empty purity that would 400 the API).
readonly property string purityCsv: {
const parts = [];
if (puritySfw) parts.push("sfw");
if (puritySketchy) parts.push("sketchy");
if (purityNsfw) parts.push("nsfw");
return parts.length > 0 ? parts.join(",") : "sfw";
}
signal downloaded(string path)
signal failed(string message)
readonly property string scriptPath:
FileUtils.trimFileProtocol(Quickshell.shellPath("scripts/wallpaper/download_wallhaven.sh"))
Component.onCompleted: readPurity.running = true
// Load current purity from config.json into the three flags.
Process {
id: readPurity
command: ["jq", "-r", ".background.wallhaven.purity // \"sfw,sketchy\"", root.configPath]
stdout: StdioCollector {
onStreamFinished: {
const csv = text.trim();
root.puritySfw = csv.indexOf("sfw") >= 0;
root.puritySketchy = csv.indexOf("sketchy") >= 0;
root.purityNsfw = csv.indexOf("nsfw") >= 0;
}
}
}
// Persist the current flags back to config.json (jq in-place via temp).
function savePurity() {
savePurityProc.exec(["bash", "-c",
"f=" + Quickshell.env("HOME") + "/.config/illogical-impulse/config.json; " +
"tmp=$(mktemp); jq --arg p " + JSON.stringify(root.purityCsv) +
" '.background.wallhaven.purity = $p' \"$f\" > \"$tmp\" && mv \"$tmp\" \"$f\""]);
}
Process { id: savePurityProc }
// Fetch one random wallpaper honoring the current purity flags.
function download() {
if (root.downloading) return;
root.downloading = true;
root.lastError = "";
proc.stdoutText = "";
proc.stderrText = "";
proc.exec(["bash", root.scriptPath, root.purityCsv]);
}
Process {
id: proc
property string stdoutText: ""
property string stderrText: ""
stdout: StdioCollector { onStreamFinished: proc.stdoutText = text }
stderr: StdioCollector { onStreamFinished: proc.stderrText = text }
onExited: (exitCode, exitStatus) => {
root.downloading = false;
const path = proc.stdoutText.trim();
if (exitCode === 0 && path.length > 0) {
// Apply through the shell's IPC same path the picker uses, so
// theming + Config.options.background.wallpaperPath stay owned
// by the shell rather than written from here.
Quickshell.execDetached(["qs", "-c", "souveraine",
"ipc", "call", "wallpapers", "apply", path]);
Config.options.background.wallpaperPath = path;
root.downloaded(path);
} else {
const msg = proc.stderrText.trim() || Translation.tr("Download failed");
root.lastError = msg;
root.failed(msg);
}
}
}
}

View file

@ -1,18 +1,20 @@
BooruResponseData 1.0 BooruResponseData.qml
singleton Ai 1.0 Ai.qml
singleton AppSearch 1.0 AppSearch.qml
singleton Audio 1.0 Audio.qml
singleton Battery 1.0 Battery.qml
singleton BluetoothStatus 1.0 BluetoothStatus.qml
singleton Booru 1.0 Booru.qml
BooruResponseData 1.0 BooruResponseData.qml
singleton Brightness 1.0 Brightness.qml
singleton Cellular 1.0 Cellular.qml
singleton ClaudeUsage 1.0 ClaudeUsage.qml
singleton Cliphist 1.0 Cliphist.qml
singleton ConflictKiller 1.0 ConflictKiller.qml
singleton CrashReporter 1.0 CrashReporter.qml
singleton DateTime 1.0 DateTime.qml
singleton EasyEffects 1.0 EasyEffects.qml
singleton Emojis 1.0 Emojis.qml
singleton FileSearch 1.0 FileSearch.qml
singleton FirstRunExperience 1.0 FirstRunExperience.qml
singleton GlobalFocusGrab 1.0 GlobalFocusGrab.qml
singleton GoogleCloud 1.0 GoogleCloud.qml
@ -32,8 +34,10 @@ singleton LockContentPolicy 1.0 LockContentPolicy.qml
singleton MaterialThemeLoader 1.0 MaterialThemeLoader.qml
singleton MprisController 1.0 MprisController.qml
singleton Network 1.0 Network.qml
singleton NetworkTraffic 1.0 NetworkTraffic.qml
singleton Notifications 1.0 Notifications.qml
singleton NotifyEvents 1.0 NotifyEvents.qml
singleton PhysicalKeyboard 1.0 PhysicalKeyboard.qml
singleton PolkitService 1.0 PolkitService.qml
singleton Privacy 1.0 Privacy.qml
singleton ResourceUsage 1.0 ResourceUsage.qml
@ -53,6 +57,7 @@ singleton Translation 1.0 Translation.qml
singleton TrayService 1.0 TrayService.qml
singleton Updates 1.0 Updates.qml
singleton WallpaperAssets 1.0 WallpaperAssets.qml
singleton WallpaperDownload 1.0 WallpaperDownload.qml
singleton Wallpapers 1.0 Wallpapers.qml
singleton Weather 1.0 Weather.qml
singleton Ydotool 1.0 Ydotool.qml