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:
parent
0d06cbb983
commit
cd2150542e
6 changed files with 327 additions and 11 deletions
12
.gitignore
vendored
12
.gitignore
vendored
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
80
surfaces/quickshell/scripts/wallpaper/download_wallhaven.sh
Executable file
80
surfaces/quickshell/scripts/wallpaper/download_wallhaven.sh
Executable 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"
|
||||
114
surfaces/quickshell/services/WallpaperDownload.qml
Normal file
114
surfaces/quickshell/services/WallpaperDownload.qml
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue