From 1a5d15525793c7f85786b7ec388d7dd3df291ea2 Mon Sep 17 00:00:00 2001 From: Meinianda-L Date: Wed, 14 Oct 2026 18:09:28 +0800 Subject: [PATCH 1/9] Add version/update commands, monitor hotplug, helper rebuilds, CI - lowpaper version; lowpaper update installs the latest release over this file and restarts a running wallpaper with it. - Monitor hotplug: on X11 the watcher follows RandR screen changes; on Wayland the helper follows outputs appearing and disappearing. Both run lowpaper restore once things settle. - The Wayland helper's cache is keyed by a checksum of its source, so a new release rebuilds it instead of reusing an old binary. - CI: shellcheck, and smoke tests on headless Sway and Xvfb. --- .github/workflows/ci.yml | 26 ++++++++++++ lowpaper | 90 ++++++++++++++++++++++++++++++---------- tests/wayland.sh | 39 +++++++++++++++++ tests/x11.sh | 43 +++++++++++++++++++ 4 files changed, 175 insertions(+), 23 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100755 tests/wayland.sh create mode 100755 tests/x11.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ff3c10f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +name: ci +on: + push: + branches: [main] + pull_request: + +jobs: + shellcheck: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - run: shellcheck --severity=warning lowpaper tests/*.sh + + wayland: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - run: sudo apt-get update && sudo apt-get install -y mpv sway ffmpeg libegl-mesa0 libgl1-mesa-dri + - run: bash tests/wayland.sh + + x11: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - run: sudo apt-get update && sudo apt-get install -y mpv xvfb x11-utils x11-xserver-utils ffmpeg netcat-openbsd libgl1-mesa-dri + - run: bash tests/x11.sh diff --git a/lowpaper b/lowpaper index a24371e..6a732bc 100755 --- a/lowpaper +++ b/lowpaper @@ -14,6 +14,7 @@ mkdir -p "$CFG" "$RUN" FIT=fill INTERVAL=300 SHUFFLE=0 AUDIO=0 AUTOPAUSE=1 HWDEC= CACHE_MB=256 ICONS=1 [ -f "$CONF" ] && . "$CONF" +VERSION=0.1.1 VIDEO='mp4|mkv|webm|mov|avi|m4v|gif' IMAGE='jpg|jpeg|png|webp|bmp|avif|jxl|tif|tiff' @@ -120,15 +121,17 @@ status() { each _st } -# Pause while the focused window is fullscreen/maximized or the monitor is off; clean up if mpv dies. -# Event driven: xprop -spy reports focus and window-state changes, so it costs nothing while idle. +# Pause while the focused window is fullscreen/maximized or the monitor is off; clean up if mpv dies; +# lay the wallpaper out again when a monitor is added, removed or resized. +# Event driven: xprop -spy and xev report changes, so it costs nothing while idle. watch() { - local fifo=$RUN/watch.fifo last= s= a line root win= alive p off=0 + local fifo=$RUN/watch.fifo last= s= a line root win= rr= alive p off=0 echo $$ > "$RUN/watch.pid" rm -f "$fifo"; mkfifo "$fifo"; exec 3<>"$fifo"; rm -f "$fifo" trap 'last=' USR1 - trap 'kill $root $win 2>/dev/null; exit' TERM EXIT + trap 'kill $root $win $rr 2>/dev/null; exit' TERM EXIT xprop -root -spy -notype _NET_ACTIVE_WINDOW >&3 2>/dev/null & root=$! + have xev && { xev -root -event randr >&3 2>/dev/null & rr=$!; } while :; do if read -r -t 10 line <&3; then case $line in @@ -137,6 +140,9 @@ watch() { [ -n "$win" ] && kill "$win" 2>/dev/null; win= [[ $a == 0x* && $a != 0x0 ]] && { xprop -id "$a" -spy -notype _NET_WM_STATE >&3 2>/dev/null & win=$!; } ;; _NET_WM_STATE*) s=$line ;; + RRScreenChangeNotify*) # restore replaces this watcher; give the desktop a moment to settle first + setsid sh -c 'sleep 2; exec "$0" restore' "$SELF" /dev/null 2>&1 & + kill $rr 2>/dev/null; rr= ;; esac else # timeout or USR1 alive=0; _alive() { kill -0 "$pid" 2>/dev/null && alive=1; }; each _alive @@ -216,19 +222,16 @@ xfdesktop_over() { # restart xfdesktop with the shim; mapped last, it lands abo } # ---------- Wayland ---------- -wl_helper() { # prints the helper path, building it on first use - local bin=${XDG_CACHE_HOME:-$HOME/.cache}/lowpaper/wl1/lowpaper-wl - [ -x "$bin" ] && { echo "$bin"; return; } - have gcc || die "Wayland needs gcc once, to build a small helper (sudo apt install gcc)" - mkdir -p "${bin%/*}" - gcc -O2 -o "$bin" -x c - -l:libwayland-client.so.0 -l:libwayland-egl.so.1 -l:libEGL.so.1 -l:libmpv.so.2 <<'C' \ - || die "could not build the Wayland helper (needs libmpv2, libwayland-egl1, libegl1)" +wl_helper() { # prints the helper path, building it on first use and again whenever its source changes + local src bin dir=${XDG_CACHE_HOME:-$HOME/.cache}/lowpaper + IFS= read -r -d '' src <<'C' || true /* lowpaper-wl: libmpv on a wlr-layer-shell background surface. * Builds with gcc alone: the few wayland/EGL/libmpv declarations it needs are written out here, * so no -dev packages are required, only the runtime libraries every Wayland desktop with mpv has. * lowpaper-wl --list print output names - * lowpaper-wl OUTPUT [--nopause] [--mpv-option=value]... FILE - * Hidden-wallpaper pause: the compositor stops sending frame callbacks to a surface nobody can see. */ + * lowpaper-wl OUTPUT [--nopause] [--restore-with=LOWPAPER] [--mpv-option=value]... FILE + * Hidden-wallpaper pause: the compositor stops sending frame callbacks to a surface nobody can see. + * Monitor hotplug: with --restore-with, an output appearing or disappearing runs `LOWPAPER restore`. */ #include #include #include @@ -302,8 +305,9 @@ uint64_t mpv_render_context_update(mpv_render_context *); /* ---- state ---- */ #define MAXOUT 16 -static struct { struct wl_proxy *p; char name[64]; int scale; } out[MAXOUT]; -static int nout, wakefd[2], width, height, scale = 1, configured, dirty, autopaused, nopause, hwdec; +static struct { struct wl_proxy *p; uint32_t id; char name[64]; int scale; } out[MAXOUT]; +static int nout, cur = -1, ready, changed, wakefd[2], width, height, scale = 1, configured, dirty, autopaused, nopause, hwdec; +static const char *restore_with; static struct wl_display *dpy; static struct wl_proxy *compositor, *shell, *surface, *callback; static struct wl_egl_window *ewin; @@ -323,8 +327,15 @@ static void o_desc(void *d, struct wl_proxy *o, const char *n) {} static void (*out_l[])() = { (void (*)())o_geometry, (void (*)())o_mode, (void (*)())o_done, (void (*)())o_scale, (void (*)())o_name, (void (*)())o_desc }; +static void outputs_changed(void) { /* let lowpaper lay the wallpaper out again, once things settle */ + if (!restore_with || changed) return; + changed = 1; + if (fork() == 0) { setsid(); execl("/bin/sh", "sh", "-c", "sleep 2; exec \"$0\" restore", restore_with, (char *)NULL); _exit(1); } +} + static void g_add(void *d, struct wl_proxy *reg, uint32_t id, const char *iface, uint32_t ver) { const struct wl_interface *i = NULL; uint32_t v = 1; + if (ready) { if (!strcmp(iface, "wl_output")) outputs_changed(); return; } if (!strcmp(iface, "wl_compositor")) i = &wl_compositor_interface, v = ver < 4 ? ver : 4; else if (!strcmp(iface, "zwlr_layer_shell_v1")) i = &zwlr_layer_shell_v1_interface; else if (!strcmp(iface, "wl_output") && nout < MAXOUT) i = &wl_output_interface, v = ver < 4 ? ver : 4; @@ -332,10 +343,13 @@ static void g_add(void *d, struct wl_proxy *reg, uint32_t id, const char *iface, struct wl_proxy *p = wl_proxy_marshal_constructor_versioned(reg, 0, i, v, id, i->name, v, NULL); if (i == &wl_compositor_interface) compositor = p; else if (i == &zwlr_layer_shell_v1_interface) shell = p; - else { out[nout].p = p; out[nout].scale = 1; snprintf(out[nout].name, 64, "%d", nout); + else { out[nout].p = p; out[nout].id = id; out[nout].scale = 1; snprintf(out[nout].name, 64, "%d", nout); LISTEN(p, out_l, (void *)(intptr_t)nout); nout++; } } -static void g_remove(void *d, struct wl_proxy *reg, uint32_t id) {} +static void g_remove(void *d, struct wl_proxy *reg, uint32_t id) { + for (int i = 0; ready && i < nout; i++) + if (out[i].id == id) { outputs_changed(); if (i == cur) exit(0); } /* our monitor is gone */ +} static void (*reg_l[])() = { (void (*)())g_add, (void (*)())g_remove }; static void l_configure(void *d, struct wl_proxy *ls, uint32_t serial, uint32_t w, uint32_t h) { @@ -380,7 +394,7 @@ int main(int argc, char **argv) { if (!shell) die("this compositor has no wlr-layer-shell (GNOME: use the Hanabi extension)"); int o = -1; for (int i = 0; i < nout; i++) if (!strcmp(out[i].name, argv[1])) o = i; if (o < 0) die("output not found"); - scale = out[o].scale; + scale = out[o].scale; cur = o; ready = 1; /* mpv: lowpaper passes the same --options it gives the mpv binary on X11 */ if (!(mpv = mpv_create())) die("mpv_create failed"); @@ -391,6 +405,7 @@ int main(int argc, char **argv) { if (!strcmp(a, "--nopause")) { nopause = 1; continue; } if (strncmp(a, "--", 2)) { file = a; continue; } if (!strncmp(a, "--playlist=", 11)) { list = a + 11; continue; } + if (!strncmp(a, "--restore-with=", 15)) { restore_with = a + 15; continue; } snprintf(k, sizeof k, "%s", a + 2); eq = strchr(k, '='); const char *v = "yes"; if (eq) *eq = 0, v = eq + 1; else if (!strncmp(k, "no-", 3)) memmove(k, k + 3, strlen(k + 3) + 1), v = "no"; @@ -459,6 +474,12 @@ int main(int argc, char **argv) { } } C + bin=$dir/wl-$(printf '%s' "$src" | cksum | cut -d' ' -f1)/lowpaper-wl + [ -x "$bin" ] && { echo "$bin"; return; } + have gcc || die "Wayland needs gcc once, to build a small helper (sudo apt install gcc)" + rm -rf "$dir"/wl[0-9-]*; mkdir -p "${bin%/*}" + gcc -O2 -o "$bin" -x c - -l:libwayland-client.so.0 -l:libwayland-egl.so.1 -l:libEGL.so.1 -l:libmpv.so.2 <<< "$src" \ + || die "could not build the Wayland helper (needs libmpv2, libwayland-egl1, libegl1)" echo "$bin" } @@ -542,12 +563,13 @@ set_wallpaper() { # [options] FILE|DIR... if [ -n "${WAYLAND_DISPLAY:-}" ]; then local wl name; wl=$(wl_helper) || exit 1 - local outs; outs=$("$wl" --list) || exit 1 + local outs lead=--restore-with=$SELF; outs=$("$wl" --list) || exit 1 : > "$STATE" for name in $outs; do [ -z "$out" ] || [ "$out" = "$name" ] || continue - setsid "$wl" "$name" $([ "$AUTOPAUSE" = 1 ] || echo --nopause) "${opts[@]}" --input-ipc-server="$RUN/$name.sock" "$@" \ - /dev/null 2>&1 & + setsid "$wl" "$name" $([ "$AUTOPAUSE" = 1 ] || echo --nopause) ${lead:+"$lead"} "${opts[@]}" \ + --input-ipc-server="$RUN/$name.sock" "$@" /dev/null 2>&1 & + lead= # one helper is enough to notice a monitor change echo "$name $! - $RUN/$name.sock" >> "$STATE" done [ -s "$STATE" ] || die "output not found: ${out} (outputs: $(echo $outs))" @@ -606,20 +628,39 @@ autopause() { echo "auto-pause $([ "$AUTOPAUSE" = 1 ] && echo on || echo off)" } +update() { # replace this file with the latest release, then restart a running wallpaper with it + local url=https://github.com/Meinianda-L/lowpaper/releases/latest/download/lowpaper tmp new + [ -w "$SELF" ] && [ -w "${SELF%/*}" ] || die "cannot write $SELF; if a package manager installed it, update it there" + tmp=$(mktemp "${SELF%/*}/.lowpaper.XXXXXX") || die "cannot create a temporary file next to $SELF" + if have curl; then curl -fsSL -o "$tmp" "$url"; elif have wget; then wget -qO "$tmp" "$url"; else false; fi \ + || { rm -f "$tmp"; die "download failed (needs curl or wget): $url"; } + head -1 "$tmp" | grep -q bash && bash -n "$tmp" 2>/dev/null || { rm -f "$tmp"; die "the download is not a lowpaper script"; } + new=$(sed -n 's/^VERSION=//p' "$tmp" | head -1); new=${new:-0} # 0.1.0 had no VERSION line + if [ "$(printf '%s\n' "$new" "$VERSION" | sort -V | tail -1)" = "$VERSION" ]; then + rm -f "$tmp"; echo "lowpaper $VERSION is up to date (latest release: $new)"; return + fi + chmod 755 "$tmp" && mv -f "$tmp" "$SELF" # a new file, so a running copy keeps reading the old one + echo "updated lowpaper $VERSION -> ${new:-unknown}" + [ -s "$STATE" ] && "$SELF" restore >/dev/null && echo "wallpaper restarted with the new version" + return 0 +} + usage() { cat <"$T/sway.log" 2>&1 & +sway=$! +for _ in $(seq 50); do [ -S "$XDG_RUNTIME_DIR/wayland-1" ] && break; sleep 0.2; done +export WAYLAND_DISPLAY=wayland-1 SWAYSOCK +SWAYSOCK=$(ls "$XDG_RUNTIME_DIR"/sway-ipc.*.sock) + +check "helper builds" "$L" _build +"$L" "$T/clip.mp4"; sleep 3; st +check "plays on HEADLESS-1" grep -q '^HEADLESS-1 playing' <<< "$(st)" +"$L" pause; check "pause" grep -q paused <<< "$(st)" +"$L" resume; check "resume" grep -q playing <<< "$(st)" +check "wrong -o fails" not "$L" -o NOPE "$T/clip.mp4" +check "wrong -o keeps the wallpaper" grep -q '^HEADLESS-1 playing' <<< "$(st)" + +swaymsg create_output >/dev/null; sleep 5; st +check "added monitor gets the wallpaper" grep -q '^HEADLESS-2 playing' <<< "$(st)" +swaymsg output HEADLESS-2 unplug >/dev/null; sleep 5; st +check "removed monitor is dropped" not grep -q HEADLESS-2 <<< "$(st)" +check "remaining monitor still plays" grep -q '^HEADLESS-1 playing' <<< "$(st)" + +"$L" stop; sleep 1 +check "stop ends the helper" not pgrep -x lowpaper-wl +kill "$sway" +[ $fail = 0 ] || { echo "--- sway log"; tail -20 "$T/sway.log"; } +exit $fail diff --git a/tests/x11.sh b/tests/x11.sh new file mode 100755 index 0000000..3db6c81 --- /dev/null +++ b/tests/x11.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Smoke test on Xvfb: play, pause, a wrong -o, re-layout after a screen resize, stop. +# Needs: Xvfb, mpv, ffmpeg, python3, x11-utils (xprop, xev, xkill, xwininfo), xrandr. +set -u +L=$(readlink -f "${0%/*}/../lowpaper") +T=$(mktemp -d) +export XDG_RUNTIME_DIR=$T/run XDG_CONFIG_HOME=$T/cfg DISPLAY=:99 LIBGL_ALWAYS_SOFTWARE=1 +unset WAYLAND_DISPLAY XDG_CURRENT_DESKTOP +mkdir -m 700 "$XDG_RUNTIME_DIR" +fail=0 +check() { local d=$1; shift; if "$@"; then echo "ok $d"; else echo "FAIL $d"; fail=1; fi; } +not() { ! "$@"; } +st() { "$L" status; } +mpvpid() { awk 'NR==1{print $2}' "$XDG_RUNTIME_DIR/lowpaper-$(id -u)/state" 2>/dev/null; } +watcher() { pgrep -f "^bash $L _watch" >/dev/null; } + +ffmpeg -loglevel error -f lavfi -i testsrc=size=320x240:rate=10:duration=2 -pix_fmt yuv420p "$T/clip.mp4" +Xvfb :99 -screen 0 1280x720x24 +extension RANDR >"$T/xvfb.log" 2>&1 & +xvfb=$! +for _ in $(seq 50); do xdpyinfo >/dev/null 2>&1 && break; sleep 0.2; done +mon=$(xrandr --listmonitors | awk 'NR==2{print $NF}') + +"$L" "$T/clip.mp4"; sleep 3; st +check "plays on $mon" grep -q "^$mon playing" <<< "$(st)" +check "watcher runs" watcher +"$L" pause; check "pause" grep -q paused <<< "$(st)" +"$L" resume; check "resume" grep -q playing <<< "$(st)" +check "wrong -o fails" not "$L" -o NOPE "$T/clip.mp4" +check "wrong -o keeps the wallpaper" grep -q "^$mon playing" <<< "$(st)" + +before=$(mpvpid) +xrandr --fb 1024x768 +# restore waits up to 20s for a desktop window, and Xvfb has none +for _ in $(seq 40); do [ "$(mpvpid)" != "$before" ] && [ -n "$(mpvpid)" ] && break; sleep 1; done +st +check "screen resize lays the wallpaper out again" test "$(mpvpid)" != "$before" +check "still plays after the resize" grep -q "^$mon playing" <<< "$(st)" + +"$L" stop; sleep 1 +check "stop ends mpv" not pgrep -x mpv +check "stop ends the watcher" not watcher +kill "$xvfb" +exit $fail From ec8c187d7bd19f446fd730a766becf912460ba1f Mon Sep 17 00:00:00 2001 From: Meinianda-L Date: Wed, 14 Oct 2026 18:12:49 +0800 Subject: [PATCH 2/9] CI fixes: shellcheck warnings, mpv logs, libmpv1 fallback, test fixes --- .github/workflows/ci.yml | 3 ++- lowpaper | 43 +++++++++++++++++++++++----------------- tests/wayland.sh | 2 +- tests/x11.sh | 7 +++++-- 4 files changed, 33 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff3c10f..d3cc8f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,8 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - - run: sudo apt-get update && sudo apt-get install -y mpv sway ffmpeg libegl-mesa0 libgl1-mesa-dri + - run: sudo apt-get update && sudo apt-get install -y mpv libmpv2 sway ffmpeg libegl-mesa0 libgl1-mesa-dri + - run: ls -l /usr/lib/x86_64-linux-gnu/libmpv* || true - run: bash tests/wayland.sh x11: diff --git a/lowpaper b/lowpaper index 6a732bc..4d0c0ea 100755 --- a/lowpaper +++ b/lowpaper @@ -11,7 +11,8 @@ CFG=${XDG_CONFIG_HOME:-$HOME/.config}/lowpaper RUN=${XDG_RUNTIME_DIR:-/tmp}/lowpaper-$(id -u) CONF=$CFG/config STATE=$RUN/state LIST=$RUN/playlist.m3u AUTOSTART=$HOME/.config/autostart/lowpaper.desktop mkdir -p "$CFG" "$RUN" -FIT=fill INTERVAL=300 SHUFFLE=0 AUDIO=0 AUTOPAUSE=1 HWDEC= CACHE_MB=256 ICONS=1 +FIT=fill INTERVAL=300 SHUFFLE=0 AUDIO=0 AUTOPAUSE=1 HWDEC='' CACHE_MB=256 ICONS=1 +# shellcheck source=/dev/null [ -f "$CONF" ] && . "$CONF" VERSION=0.1.1 @@ -114,7 +115,7 @@ toggle() { if [ "$(first_get pause)" = true ]; then resume; else pause; fi; } status() { [ -s "$STATE" ] || { echo "not running"; return 1; } _st() { - kill -0 "$pid" 2>/dev/null || { echo "$name stopped"; return; } + kill -0 "$pid" 2>/dev/null || { echo "$name stopped (see $RUN/$name.log)"; return; } local r; mapfile -t r < <(ipc "$sock" '["get_property","pause"]' '["get_property","path"]') echo "$name $([ "${r[0]:-}" = true ] && echo paused || echo playing) ${r[1]:-?}" } @@ -125,7 +126,7 @@ status() { # lay the wallpaper out again when a monitor is added, removed or resized. # Event driven: xprop -spy and xev report changes, so it costs nothing while idle. watch() { - local fifo=$RUN/watch.fifo last= s= a line root win= rr= alive p off=0 + local fifo=$RUN/watch.fifo last='' s='' a line root win='' rr='' alive p off=0 echo $$ > "$RUN/watch.pid" rm -f "$fifo"; mkfifo "$fifo"; exec 3<>"$fifo"; rm -f "$fifo" trap 'last=' USR1 @@ -149,6 +150,7 @@ watch() { [ $alive = 1 ] || { stop; exit; } off=0; xset q 2>/dev/null | grep -q 'Monitor is Off' && off=1 fi + # shellcheck source=/dev/null [ -f "$CONF" ] && . "$CONF" if [ "$AUTOPAUSE" != 1 ] || [ -e "$RUN/paused" ]; then last=; continue; fi p=false; [ $off = 1 ] && p=true @@ -285,11 +287,11 @@ EGLContext eglCreateContext(EGLDisplay, EGLConfig, EGLContext, const EGLint *); EGLSurface eglCreateWindowSurface(EGLDisplay, EGLConfig, void *, const EGLint *); void *eglGetProcAddress(const char *); -/* ---- libmpv (client API 2) ---- */ +/* ---- libmpv (client API 1 or 2) ---- */ typedef struct mpv_handle mpv_handle; typedef struct mpv_render_context mpv_render_context; typedef struct { int event_id, error; uint64_t reply_userdata; void *data; } mpv_event; typedef struct { int type; void *data; } mpv_render_param; -typedef struct { void *(*get_proc_address)(void *, const char *); void *ctx; } mpv_opengl_init_params; +typedef struct { void *(*get_proc_address)(void *, const char *); void *ctx; const char *extra_exts; } mpv_opengl_init_params; /* extra_exts: API 1 only */ typedef struct { int fbo, w, h, internal_format; } mpv_opengl_fbo; mpv_handle *mpv_create(void); int mpv_set_option_string(mpv_handle *, const char *, const char *), mpv_initialize(mpv_handle *), @@ -446,7 +448,7 @@ int main(int argc, char **argv) { if (!esurf || !eglMakeCurrent(edpy, esurf, esurf, ctx)) die("EGL surface failed"); eglSwapInterval(edpy, 0); /* pacing comes from frame callbacks, never block in swap */ - mpv_opengl_init_params gl = { gpa, NULL }; + mpv_opengl_init_params gl = { gpa, NULL, NULL }; mpv_render_param rp[] = { { 1, "opengl" }, { 2, &gl }, { hwdec ? 9 : 0, dpy }, { 0, NULL } }; /* 9: WL_DISPLAY, for hwdec */ if (mpv_render_context_create(&rctx, mpv, rp) < 0) die("mpv render context failed"); if (pipe(wakefd)) die("pipe"); @@ -478,8 +480,11 @@ C [ -x "$bin" ] && { echo "$bin"; return; } have gcc || die "Wayland needs gcc once, to build a small helper (sudo apt install gcc)" rm -rf "$dir"/wl[0-9-]*; mkdir -p "${bin%/*}" - gcc -O2 -o "$bin" -x c - -l:libwayland-client.so.0 -l:libwayland-egl.so.1 -l:libEGL.so.1 -l:libmpv.so.2 <<< "$src" \ - || die "could not build the Wayland helper (needs libmpv2, libwayland-egl1, libegl1)" + local m; for m in 2 1 ''; do # libmpv.so.2 (mpv >= 0.35), .so.1 on older distros + [ -n "$m" ] || die "could not build the Wayland helper (needs libmpv2 or libmpv1, libwayland-egl1, libegl1; see $dir/build.log)" + gcc -O2 -o "$bin" -x c - -l:libwayland-client.so.0 -l:libwayland-egl.so.1 -l:libEGL.so.1 -l:libmpv.so.$m \ + <<< "$src" 2>"$dir/build.log" && break + done echo "$bin" } @@ -504,7 +509,7 @@ frame_cache() { # a short loop fits in RAM: decode it once, then replay the fra } set_wallpaper() { # [options] FILE|DIR... - local out= mpvonly=0 a f n abs=() fo + local out='' mpvonly=0 a f n abs=() fo fit last while [ $# -gt 0 ]; do case $1 in -f|--fit) FIT=$2; shift 2 ;; -i|--interval) INTERVAL=$2; shift 2 ;; @@ -518,7 +523,7 @@ set_wallpaper() { # [options] FILE|DIR... esac; done [ $# -gt 0 ] || die "need a file or folder" have mpv || die "mpv is required" - fo=($(fit_opts "$FIT")) || die "--fit must be fill|fit|stretch" + fit=$(fit_opts "$FIT") || die "--fit must be fill|fit|stretch"; read -r -a fo <<< "$fit" if [ -n "$out" ]; then # check before stopping the current wallpaper or touching `last` local names if [ -n "${WAYLAND_DISPLAY:-}" ]; then local wl; wl=$(wl_helper) || exit 1; names=$("$wl" --list) || exit 1 @@ -537,8 +542,9 @@ set_wallpaper() { # [options] FILE|DIR... n=$(wc -l < "$LIST.new"); [ "$n" -gt 0 ] || die "no images or videos found" mv "$LIST.new" "$LIST"; f=$(head -1 "$LIST") # remember for `lowpaper restore` (autostart) - printf '%q ' -f "$FIT" -i "$INTERVAL" $([ $SHUFFLE = 1 ] && echo -s) $([ $AUDIO = 1 ] && echo -a) \ - ${out:+-o "$out"} $([ $mpvonly = 1 ] && echo --mpv) -- "${abs[@]}" > "$CFG/last" + last=(-f "$FIT" -i "$INTERVAL"); [ "$SHUFFLE" = 1 ] && last+=(-s); [ "$AUDIO" = 1 ] && last+=(-a) + [ -n "$out" ] && last+=(-o "$out"); [ $mpvonly = 1 ] && last+=(--mpv) + printf '%q ' "${last[@]}" -- "${abs[@]}" > "$CFG/last" if [ "$n" = 1 ] && is_image "$f" && [ $mpvonly = 0 ] && [ -z "${WAYLAND_DISPLAY:-}" ] && xfce; then stop; native_image "$f" @@ -546,7 +552,7 @@ set_wallpaper() { # [options] FILE|DIR... fi stop keep - local opts=(--no-config --no-terminal --osc=no --osd-level=0 --load-scripts=no --ytdl=no + local opts=(--no-config --terminal=yes --input-terminal=no --msg-level=all=warn --msg-color=no --term-osd=no --osc=no --osd-level=0 --load-scripts=no --ytdl=no --input-default-bindings=no --input-vo-keyboard=no --cursor-autohide=always --stop-screensaver=no --sid=no --profile=fast --framedrop=vo --cache=no --demuxer-max-bytes=4MiB --demuxer-max-back-bytes=0 --panscan="${fo[0]}" --keepaspect="${fo[1]}") @@ -563,12 +569,13 @@ set_wallpaper() { # [options] FILE|DIR... if [ -n "${WAYLAND_DISPLAY:-}" ]; then local wl name; wl=$(wl_helper) || exit 1 - local outs lead=--restore-with=$SELF; outs=$("$wl" --list) || exit 1 + local outs lead=--restore-with=$SELF np=''; outs=$("$wl" --list) || exit 1 + [ "$AUTOPAUSE" = 1 ] || np=--nopause : > "$STATE" for name in $outs; do [ -z "$out" ] || [ "$out" = "$name" ] || continue - setsid "$wl" "$name" $([ "$AUTOPAUSE" = 1 ] || echo --nopause) ${lead:+"$lead"} "${opts[@]}" \ - --input-ipc-server="$RUN/$name.sock" "$@" /dev/null 2>&1 & + setsid "$wl" "$name" ${np:+"$np"} ${lead:+"$lead"} "${opts[@]}" \ + --input-ipc-server="$RUN/$name.sock" "$@" "$RUN/$name.log" 2>&1 & lead= # one helper is enough to notice a monitor change echo "$name $! - $RUN/$name.sock" >> "$STATE" done @@ -586,7 +593,7 @@ set_wallpaper() { # [options] FILE|DIR... [ -n "$wid" ] && xwininfo -id "$wid" >/dev/null 2>&1 \ || { wid=$(py win "$x" "$y" "$w" "$h" ${so:+under}) || die "could not create window"; new=new; } sock=$RUN/$name.sock - setsid mpv "${opts[@]}" --vo=xv,gpu --wid="$wid" --input-ipc-server="$sock" "$@" /dev/null 2>&1 & + setsid mpv "${opts[@]}" --vo=xv,gpu --wid="$wid" --input-ipc-server="$sock" "$@" "$RUN/$name.log" 2>&1 & echo "$name $! $wid $sock" >> "$STATE" done < <(monitors) for wid in "${old[@]}"; do xkill -id "$wid" >/dev/null 2>&1; done @@ -598,7 +605,7 @@ set_wallpaper() { # [options] FILE|DIR... } set_fit() { # change fit live and remember it - local fo; fo=($(fit_opts "$1")) || die "fit must be fill|fit|stretch" + local fit fo; fit=$(fit_opts "$1") || die "fit must be fill|fit|stretch"; read -r -a fo <<< "$fit" conf_set FIT "$1" ipc_all "[\"set_property\",\"panscan\",${fo[0]}]"; ipc_all "[\"set_property\",\"keepaspect\",$([ "${fo[1]}" = yes ] && echo true || echo false)]" [ -f "$CFG/last" ] && sed -i "s/^-f [a-z]*/-f $1/" "$CFG/last" diff --git a/tests/wayland.sh b/tests/wayland.sh index ae32603..d9cd48a 100755 --- a/tests/wayland.sh +++ b/tests/wayland.sh @@ -35,5 +35,5 @@ check "remaining monitor still plays" grep -q '^HEADLESS-1 playing' <<< "$(st)" "$L" stop; sleep 1 check "stop ends the helper" not pgrep -x lowpaper-wl kill "$sway" -[ $fail = 0 ] || { echo "--- sway log"; tail -20 "$T/sway.log"; } +[ $fail = 0 ] || { tail -n 20 "$T/sway.log" "$XDG_RUNTIME_DIR"/lowpaper-*/*.log "$XDG_CACHE_HOME"/lowpaper/build.log 2>/dev/null; } exit $fail diff --git a/tests/x11.sh b/tests/x11.sh index 3db6c81..02ce9be 100755 --- a/tests/x11.sh +++ b/tests/x11.sh @@ -29,15 +29,18 @@ check "wrong -o fails" not "$L" -o NOPE "$T/clip.mp4" check "wrong -o keeps the wallpaper" grep -q "^$mon playing" <<< "$(st)" before=$(mpvpid) -xrandr --fb 1024x768 +# Xvfb has one mode; add a smaller one and switch to it, which changes the screen size +xrandr --newmode 1024x768 63.50 1024 1072 1176 1328 768 771 775 798 -hsync +vsync +xrandr --addmode "$mon" 1024x768 && xrandr --output "$mon" --mode 1024x768 # restore waits up to 20s for a desktop window, and Xvfb has none for _ in $(seq 40); do [ "$(mpvpid)" != "$before" ] && [ -n "$(mpvpid)" ] && break; sleep 1; done st -check "screen resize lays the wallpaper out again" test "$(mpvpid)" != "$before" +check "screen resize lays the wallpaper out again" test -n "$(mpvpid)" -a "$(mpvpid)" != "$before" check "still plays after the resize" grep -q "^$mon playing" <<< "$(st)" "$L" stop; sleep 1 check "stop ends mpv" not pgrep -x mpv check "stop ends the watcher" not watcher kill "$xvfb" +[ $fail = 0 ] || tail -n 20 "$XDG_RUNTIME_DIR"/lowpaper-*/*.log 2>/dev/null exit $fail From 6d71267566c656647a426928c6a158d0d099e84a Mon Sep 17 00:00:00 2001 From: Meinianda-L Date: Wed, 14 Oct 2026 18:14:59 +0800 Subject: [PATCH 3/9] Fall back to vo=x11 when neither Xv nor OpenGL works; rotate in the X11 test --- lowpaper | 2 +- tests/x11.sh | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lowpaper b/lowpaper index 4d0c0ea..2f969b3 100755 --- a/lowpaper +++ b/lowpaper @@ -593,7 +593,7 @@ set_wallpaper() { # [options] FILE|DIR... [ -n "$wid" ] && xwininfo -id "$wid" >/dev/null 2>&1 \ || { wid=$(py win "$x" "$y" "$w" "$h" ${so:+under}) || die "could not create window"; new=new; } sock=$RUN/$name.sock - setsid mpv "${opts[@]}" --vo=xv,gpu --wid="$wid" --input-ipc-server="$sock" "$@" "$RUN/$name.log" 2>&1 & + setsid mpv "${opts[@]}" --vo=xv,gpu,x11 --wid="$wid" --input-ipc-server="$sock" "$@" "$RUN/$name.log" 2>&1 & echo "$name $! $wid $sock" >> "$STATE" done < <(monitors) for wid in "${old[@]}"; do xkill -id "$wid" >/dev/null 2>&1; done diff --git a/tests/x11.sh b/tests/x11.sh index 02ce9be..fcc7af1 100755 --- a/tests/x11.sh +++ b/tests/x11.sh @@ -29,9 +29,8 @@ check "wrong -o fails" not "$L" -o NOPE "$T/clip.mp4" check "wrong -o keeps the wallpaper" grep -q "^$mon playing" <<< "$(st)" before=$(mpvpid) -# Xvfb has one mode; add a smaller one and switch to it, which changes the screen size -xrandr --newmode 1024x768 63.50 1024 1072 1176 1328 768 771 775 798 -hsync +vsync -xrandr --addmode "$mon" 1024x768 && xrandr --output "$mon" --mode 1024x768 +# Xvfb has a single mode, so change the screen size by rotating the output +xrandr --output "$mon" --rotate left # restore waits up to 20s for a desktop window, and Xvfb has none for _ in $(seq 40); do [ "$(mpvpid)" != "$before" ] && [ -n "$(mpvpid)" ] && break; sleep 1; done st From 33e1f39436ba63117c735438b46be9a2099313d8 Mon Sep 17 00:00:00 2001 From: Meinianda-L Date: Wed, 14 Oct 2026 18:16:58 +0800 Subject: [PATCH 4/9] X11 test: resize the screen the way Xvfb allows --- tests/x11.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/x11.sh b/tests/x11.sh index fcc7af1..40af911 100755 --- a/tests/x11.sh +++ b/tests/x11.sh @@ -29,8 +29,8 @@ check "wrong -o fails" not "$L" -o NOPE "$T/clip.mp4" check "wrong -o keeps the wallpaper" grep -q "^$mon playing" <<< "$(st)" before=$(mpvpid) -# Xvfb has a single mode, so change the screen size by rotating the output -xrandr --output "$mon" --rotate left +# Xvfb has a single mode: switch the output off, shrink the screen, switch it back on +xrandr --output "$mon" --off --fb 1024x768 && xrandr --output "$mon" --auto # restore waits up to 20s for a desktop window, and Xvfb has none for _ in $(seq 40); do [ "$(mpvpid)" != "$before" ] && [ -n "$(mpvpid)" ] && break; sleep 1; done st From efe5455d4bf2147cb25166841e2d693888b02460 Mon Sep 17 00:00:00 2001 From: Meinianda-L Date: Wed, 14 Oct 2026 18:19:35 +0800 Subject: [PATCH 5/9] X11 test: use Xorg's dummy driver so the resolution change can be tested --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3cc8f0..2c1f717 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,12 +16,11 @@ jobs: steps: - uses: actions/checkout@v4 - run: sudo apt-get update && sudo apt-get install -y mpv libmpv2 sway ffmpeg libegl-mesa0 libgl1-mesa-dri - - run: ls -l /usr/lib/x86_64-linux-gnu/libmpv* || true - run: bash tests/wayland.sh x11: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - - run: sudo apt-get update && sudo apt-get install -y mpv xvfb x11-utils x11-xserver-utils ffmpeg netcat-openbsd libgl1-mesa-dri + - run: sudo apt-get update && sudo apt-get install -y mpv xvfb xserver-xorg-core xserver-xorg-video-dummy x11-utils x11-xserver-utils ffmpeg netcat-openbsd libgl1-mesa-dri - run: bash tests/x11.sh From a24db5c0590d2b37b35728d328b90c994714290b Mon Sep 17 00:00:00 2001 From: Meinianda-L Date: Wed, 14 Oct 2026 18:21:42 +0800 Subject: [PATCH 6/9] X11 test: use Xorg's dummy driver so the resolution change can be tested --- tests/x11.sh | 42 ++++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/tests/x11.sh b/tests/x11.sh index 40af911..a0b5f93 100755 --- a/tests/x11.sh +++ b/tests/x11.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash -# Smoke test on Xvfb: play, pause, a wrong -o, re-layout after a screen resize, stop. -# Needs: Xvfb, mpv, ffmpeg, python3, x11-utils (xprop, xev, xkill, xwininfo), xrandr. +# Smoke test on a headless X server: play, pause, a wrong -o, re-layout after a resolution change, stop. +# Needs: mpv, ffmpeg, python3, x11-utils (xprop, xev, xkill, xwininfo), xrandr, and Xorg's dummy driver +# (falls back to Xvfb, which cannot change resolution, so the resize check is skipped there). set -u L=$(readlink -f "${0%/*}/../lowpaper") T=$(mktemp -d) @@ -15,8 +16,23 @@ mpvpid() { awk 'NR==1{print $2}' "$XDG_RUNTIME_DIR/lowpaper-$(id -u)/state" 2>/d watcher() { pgrep -f "^bash $L _watch" >/dev/null; } ffmpeg -loglevel error -f lavfi -i testsrc=size=320x240:rate=10:duration=2 -pix_fmt yuv420p "$T/clip.mp4" -Xvfb :99 -screen 0 1280x720x24 +extension RANDR >"$T/xvfb.log" 2>&1 & -xvfb=$! +if [ -e /usr/lib/xorg/modules/drivers/dummy_drv.so ] && sudo -n true 2>/dev/null; then + printf '%s\n' \ + 'Section "Device"' ' Identifier "d"' ' Driver "dummy"' ' VideoRam 64000' 'EndSection' \ + 'Section "Monitor"' ' Identifier "m"' ' HorizSync 5.0-1000.0' ' VertRefresh 5.0-200.0' \ + ' Modeline "1280x720" 74.25 1280 1390 1430 1650 720 725 730 750 +hsync +vsync' \ + ' Modeline "1024x768" 65.00 1024 1048 1184 1344 768 771 777 806 -hsync -vsync' 'EndSection' \ + 'Section "Screen"' ' Identifier "s"' ' Device "d"' ' Monitor "m"' ' DefaultDepth 24' \ + ' SubSection "Display"' ' Depth 24' ' Modes "1280x720" "1024x768"' ' EndSubSection' 'EndSection' \ + > "$T/dummy.conf" + sudo Xorg :99 -noreset -config "$T/dummy.conf" -logfile "$T/xorg.log" >/dev/null 2>&1 & + resize() { xrandr -s 1024x768; } + stopx() { sudo pkill -x Xorg; } +else + Xvfb :99 -screen 0 1280x720x24 +extension RANDR >"$T/xvfb.log" 2>&1 & + resize() { return 1; } + stopx() { pkill -x Xvfb; } +fi for _ in $(seq 50); do xdpyinfo >/dev/null 2>&1 && break; sleep 0.2; done mon=$(xrandr --listmonitors | awk 'NR==2{print $NF}') @@ -29,17 +45,19 @@ check "wrong -o fails" not "$L" -o NOPE "$T/clip.mp4" check "wrong -o keeps the wallpaper" grep -q "^$mon playing" <<< "$(st)" before=$(mpvpid) -# Xvfb has a single mode: switch the output off, shrink the screen, switch it back on -xrandr --output "$mon" --off --fb 1024x768 && xrandr --output "$mon" --auto -# restore waits up to 20s for a desktop window, and Xvfb has none -for _ in $(seq 40); do [ "$(mpvpid)" != "$before" ] && [ -n "$(mpvpid)" ] && break; sleep 1; done -st -check "screen resize lays the wallpaper out again" test -n "$(mpvpid)" -a "$(mpvpid)" != "$before" -check "still plays after the resize" grep -q "^$mon playing" <<< "$(st)" +if resize; then + # restore waits up to 20s for a desktop window, and there is none here + for _ in $(seq 40); do [ "$(mpvpid)" != "$before" ] && [ -n "$(mpvpid)" ] && break; sleep 1; done + st + check "resolution change lays the wallpaper out again" test -n "$(mpvpid)" -a "$(mpvpid)" != "$before" + check "still plays after the change" grep -q "^$mon playing" <<< "$(st)" +else + echo "skip resolution change (needs Xorg with the dummy driver)" +fi "$L" stop; sleep 1 check "stop ends mpv" not pgrep -x mpv check "stop ends the watcher" not watcher -kill "$xvfb" +stopx [ $fail = 0 ] || tail -n 20 "$XDG_RUNTIME_DIR"/lowpaper-*/*.log 2>/dev/null exit $fail From a606a5eb7e68c0184c01312aba8e40f41c2a18c3 Mon Sep 17 00:00:00 2001 From: Meinianda-L Date: Wed, 14 Oct 2026 18:24:06 +0800 Subject: [PATCH 7/9] Fix a race on monitor changes: act on the first RandR event only, serialize set_wallpaper with flock --- lowpaper | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lowpaper b/lowpaper index 2f969b3..7b2a926 100755 --- a/lowpaper +++ b/lowpaper @@ -141,9 +141,9 @@ watch() { [ -n "$win" ] && kill "$win" 2>/dev/null; win= [[ $a == 0x* && $a != 0x0 ]] && { xprop -id "$a" -spy -notype _NET_WM_STATE >&3 2>/dev/null & win=$!; } ;; _NET_WM_STATE*) s=$line ;; - RRScreenChangeNotify*) # restore replaces this watcher; give the desktop a moment to settle first - setsid sh -c 'sleep 2; exec "$0" restore' "$SELF" /dev/null 2>&1 & - kill $rr 2>/dev/null; rr= ;; + RRScreenChangeNotify*) # a change comes as several events: act once. restore replaces this watcher + [ -n "$rr" ] && { kill "$rr" 2>/dev/null; rr='' + setsid sh -c 'sleep 2; exec "$0" restore' "$SELF" /dev/null 2>&1 & } ;; esac else # timeout or USR1 alive=0; _alive() { kill -0 "$pid" 2>/dev/null && alive=1; }; each _alive @@ -220,7 +220,7 @@ xfdesktop_over() { # restart xfdesktop with the shim; mapped last, it lands abo local p; p=$(pgrep -xo xfdesktop) && grep -qs "$1" "/proc/$p/maps" && [ "${2:-}" != new ] && return xfdesktop --quit 2>/dev/null for _ in $(seq 30); do pgrep -x xfdesktop >/dev/null || break; sleep 0.1; done - LD_PRELOAD=$1 setsid xfdesktop /dev/null 2>&1 & + LD_PRELOAD=$1 setsid xfdesktop /dev/null 2>&1 9>&- & } # ---------- Wayland ---------- @@ -510,6 +510,7 @@ frame_cache() { # a short loop fits in RAM: decode it once, then replay the fra set_wallpaper() { # [options] FILE|DIR... local out='' mpvonly=0 a f n abs=() fo fit last + have flock && { exec 9>"$RUN/lock"; flock 9; } # one at a time (a hotplug restore can meet a user command) while [ $# -gt 0 ]; do case $1 in -f|--fit) FIT=$2; shift 2 ;; -i|--interval) INTERVAL=$2; shift 2 ;; @@ -575,7 +576,7 @@ set_wallpaper() { # [options] FILE|DIR... for name in $outs; do [ -z "$out" ] || [ "$out" = "$name" ] || continue setsid "$wl" "$name" ${np:+"$np"} ${lead:+"$lead"} "${opts[@]}" \ - --input-ipc-server="$RUN/$name.sock" "$@" "$RUN/$name.log" 2>&1 & + --input-ipc-server="$RUN/$name.sock" "$@" "$RUN/$name.log" 2>&1 9>&- & lead= # one helper is enough to notice a monitor change echo "$name $! - $RUN/$name.sock" >> "$STATE" done @@ -593,13 +594,14 @@ set_wallpaper() { # [options] FILE|DIR... [ -n "$wid" ] && xwininfo -id "$wid" >/dev/null 2>&1 \ || { wid=$(py win "$x" "$y" "$w" "$h" ${so:+under}) || die "could not create window"; new=new; } sock=$RUN/$name.sock - setsid mpv "${opts[@]}" --vo=xv,gpu,x11 --wid="$wid" --input-ipc-server="$sock" "$@" "$RUN/$name.log" 2>&1 & + setsid mpv "${opts[@]}" --vo=xv,gpu,x11 --wid="$wid" --input-ipc-server="$sock" "$@" "$RUN/$name.log" 2>&1 9>&- & echo "$name $! $wid $sock" >> "$STATE" done < <(monitors) for wid in "${old[@]}"; do xkill -id "$wid" >/dev/null 2>&1; done [ -s "$STATE" ] || die "monitor not found: ${out}" [ -n "$so" ] && xfdesktop_over "$so" $new - setsid "$SELF" _watch /dev/null 2>&1 & + setsid "$SELF" _watch /dev/null 2>&1 9>&- & + echo $! > "$RUN/watch.pid" # now, so a stop right after this cannot miss it fi echo "Wallpaper started ($n file(s)): $f" } From 174717a6f089b8d54d71866e7ad0d4346a4f8530 Mon Sep 17 00:00:00 2001 From: Meinianda-L Date: Wed, 14 Oct 2026 18:27:07 +0800 Subject: [PATCH 8/9] README: other distros, update/version, hotplug; test lowpaper update in CI --- .github/workflows/ci.yml | 6 ++++++ README.md | 26 +++++++++++++++++++------- tests/update.sh | 20 ++++++++++++++++++++ 3 files changed, 45 insertions(+), 7 deletions(-) create mode 100755 tests/update.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c1f717..cb83e01 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,3 +24,9 @@ jobs: - uses: actions/checkout@v4 - run: sudo apt-get update && sudo apt-get install -y mpv xvfb xserver-xorg-core xserver-xorg-video-dummy x11-utils x11-xserver-utils ffmpeg netcat-openbsd libgl1-mesa-dri - run: bash tests/x11.sh + + update: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - run: bash tests/update.sh diff --git a/README.md b/README.md index 49fe29e..9199e5d 100644 --- a/README.md +++ b/README.md @@ -33,14 +33,23 @@ ## Install +Install mpv with your package manager: + +```sh +sudo apt install mpv # Debian, Ubuntu, Mint +sudo pacman -S mpv # Arch +sudo dnf install mpv # Fedora (if dnf cannot find it, enable RPM Fusion) +``` + +Then download lowpaper: + ```sh -sudo apt install mpv mkdir -p ~/.local/bin curl -Lo ~/.local/bin/lowpaper https://github.com/Meinianda-L/lowpaper/releases/latest/download/lowpaper chmod +x ~/.local/bin/lowpaper ``` -lowpaper is a single file, so that is the whole install. The same script handles X11 and Wayland. To remove it, run `lowpaper stop` and delete the file. +lowpaper is a single file, so that is the whole install. The same script handles X11 and Wayland. `lowpaper update` replaces it with the latest release. To remove it, run `lowpaper stop` and delete the file. ## Usage @@ -55,6 +64,8 @@ lowpaper pause | resume | toggle | next | stop | status | restore lowpaper fit fill|fit|stretch # change scaling live lowpaper autopause on|off # pause while the wallpaper is covered (default on) lowpaper startup on|off # start at login, restoring the last wallpaper +lowpaper update # install the latest release +lowpaper version ``` Settings live in `~/.config/lowpaper/config` (`FIT`, `INTERVAL`, `AUTOPAUSE`, `CACHE_MB`, `ICONS`, `HWDEC`). @@ -64,8 +75,8 @@ Settings live in `~/.config/lowpaper/config` (`FIT`, `INTERVAL`, `AUTOPAUSE`, `C | Environment | Needs | |---|---| | all | mpv | -| X11 desktop with a desktop window (XFCE, MATE, Cinnamon, ...) | python3, xprop, xrandr, xkill (preinstalled on Debian, Ubuntu, Mint) | -| Wayland with layer-shell (Sway, Hyprland, KDE, ...) | gcc, used once to build a 30 KB helper | +| X11 desktop with a desktop window (XFCE, MATE, Cinnamon, ...) | python3, xprop, xrandr, xkill, xev (preinstalled on Debian, Ubuntu, Mint; on Arch: `python xorg-xprop xorg-xrandr xorg-xkill xorg-xev`) | +| Wayland with layer-shell (Sway, Hyprland, KDE, ...) | gcc, used once to build a 30 KB helper, and libmpv (Debian/Ubuntu: `libmpv2`, Fedora: `mpv-libs`, included in Arch's `mpv`) | Optional: `gcc` also keeps XFCE desktop icons above the video. OpenBSD `nc` (default on Debian and Ubuntu) is used for mpv IPC, about 3 ms per call; without it lowpaper falls back to python, which is much slower on old CPUs. @@ -73,9 +84,9 @@ GNOME on Wayland has no layer-shell; use the Hanabi extension there. Bare window ## How it works -**X11.** A one-shot python call creates a desktop-type window and exits. mpv draws into it with `--wid`, using Xv where available, which is the cheapest path on old GPUs. A small Bash watcher follows focus and window-state changes through `xprop -spy`, so it sleeps until something actually changes, and pauses mpv while a fullscreen or maximized window is on top or the monitor is off. +**X11.** A one-shot python call creates a desktop-type window and exits. mpv draws into it with `--wid`, using Xv where available, which is the cheapest path on old GPUs. A small Bash watcher follows focus and window-state changes through `xprop -spy`, so it sleeps until something actually changes, and pauses mpv while a fullscreen or maximized window is on top or the monitor is off. It also listens for RandR screen changes through `xev`, and lays the wallpaper out again when a monitor is added, removed or resized. -**Wayland.** lowpaper builds a small C helper the first time it is needed and keeps it in `~/.cache/lowpaper`. The helper speaks the wlr-layer-shell protocol directly, places one background surface per output, and renders through libmpv. It declares the few Wayland, EGL and libmpv functions it uses itself, so no `-dev` packages are needed: only gcc and the libraries mpv already depends on. A compositor stops sending frame callbacks to a surface nobody can see; the helper uses that to pause. +**Wayland.** lowpaper builds a small C helper the first time it is needed and keeps it in `~/.cache/lowpaper`. The helper speaks the wlr-layer-shell protocol directly, places one background surface per output, and renders through libmpv. It declares the few Wayland, EGL and libmpv functions it uses itself, so no `-dev` packages are needed: only gcc and the libraries mpv already depends on. A compositor stops sending frame callbacks to a surface nobody can see; the helper uses that to pause. When an output appears or disappears, it lays the wallpaper out again. **Short clips** are decoded once and replayed from RAM, which takes the decoder out of the loop. Longer videos fall back to normal decoding. `CACHE_MB` sets the limit. @@ -95,5 +106,6 @@ Linux Mint 22.3 XFCE/X11, Core2 Duo P8600, GeForce 320M (nouveau, no usable hwde ## Known limits -- After a resolution or monitor change, or if xfdesktop restarts on its own, run `lowpaper restore`. +- If xfdesktop restarts on its own, run `lowpaper restore`. +- If `lowpaper status` says `stopped`, mpv's error is in the log file it names. - On Wayland, `lowpaper autopause on|off` applies from the next start. diff --git a/tests/update.sh b/tests/update.sh new file mode 100755 index 0000000..89f17a5 --- /dev/null +++ b/tests/update.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# lowpaper update: an older copy is replaced by the latest release, which must still be a working script. +# Needs network access to github.com. +set -u +T=$(mktemp -d) +export XDG_RUNTIME_DIR=$T/run XDG_CONFIG_HOME=$T/cfg +mkdir -m 700 "$XDG_RUNTIME_DIR" "$T/bin" +fail=0 +check() { local d=$1; shift; if "$@"; then echo "ok $d"; else echo "FAIL $d"; fail=1; fi; } + +sed 's/^VERSION=.*/VERSION=0.0.1/' "${0%/*}/../lowpaper" > "$T/bin/lowpaper"; chmod +x "$T/bin/lowpaper" +out=$("$T/bin/lowpaper" update); echo "$out" +check "update reports what it did" grep -qE '^(updated lowpaper 0.0.1 -> |lowpaper 0.0.1 is up to date)' <<< "$out" +check "the file is still a valid script" bash -n "$T/bin/lowpaper" +check "the file is executable" test -x "$T/bin/lowpaper" +check "no temporary file is left" test -z "$(find "$T/bin" -name '.lowpaper.*')" +cp "$T/bin/lowpaper" "$T/bin/again" +out=$("$T/bin/again" update); echo "$out" +check "a current copy is left alone" grep -q 'is up to date' <<< "$out" +exit $fail From 2cb9a6ec59c03ca6d37baf570cdb60697e2aa28f Mon Sep 17 00:00:00 2001 From: Meinianda-L Date: Wed, 14 Oct 2026 18:29:06 +0800 Subject: [PATCH 9/9] AUR package files, checked by CI when a release is published --- .github/workflows/ci.yml | 17 +++++++++++++++++ packaging/aur/.SRCINFO | 24 ++++++++++++++++++++++++ packaging/aur/PKGBUILD | 29 +++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 packaging/aur/.SRCINFO create mode 100644 packaging/aur/PKGBUILD diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb83e01..6136e54 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,9 @@ on: push: branches: [main] pull_request: + release: + types: [published] + workflow_dispatch: jobs: shellcheck: @@ -30,3 +33,17 @@ jobs: steps: - uses: actions/checkout@v4 - run: bash tests/update.sh + + aur: # the PKGBUILD downloads the release, so it can only be checked once one is published + if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-24.04 + container: archlinux:latest + steps: + - uses: actions/checkout@v4 + - run: pacman -Syu --noconfirm --needed base-devel + - run: | + useradd -m builder + cp -r packaging/aur /home/builder/pkg && chown -R builder /home/builder/pkg + su builder -c 'cd ~/pkg && makepkg --nodeps --noconfirm && makepkg --printsrcinfo | diff - .SRCINFO' + pacman -Udd --noconfirm /home/builder/pkg/lowpaper-*.pkg.tar.zst + lowpaper version diff --git a/packaging/aur/.SRCINFO b/packaging/aur/.SRCINFO new file mode 100644 index 0000000..f99ad02 --- /dev/null +++ b/packaging/aur/.SRCINFO @@ -0,0 +1,24 @@ +pkgbase = lowpaper + pkgdesc = Lightweight video wallpaper for X11 and Wayland, made for low-end and old hardware + pkgver = 0.1.1 + pkgrel = 1 + url = https://github.com/Meinianda-L/lowpaper + arch = any + license = MIT + depends = bash + depends = mpv + optdepends = python: X11 support + optdepends = xorg-xprop: X11 support + optdepends = xorg-xrandr: X11 support + optdepends = xorg-xkill: X11 support + optdepends = xorg-xwininfo: X11 support + optdepends = xorg-xev: X11 monitor hotplug + optdepends = xorg-xset: X11 pause while the monitor is off + optdepends = gcc: Wayland support, and XFCE desktop icons above the video + optdepends = openbsd-netcat: faster control of mpv + source = lowpaper-0.1.1::https://github.com/Meinianda-L/lowpaper/releases/download/v0.1.1/lowpaper + source = LICENSE-0.1.1::https://raw.githubusercontent.com/Meinianda-L/lowpaper/v0.1.1/LICENSE + sha256sums = f2596b647c1d7241264712c5027a7644b6b9b0d7166f6b91691f62236c198d25 + sha256sums = 0a09c24d8e883bdf2a32da4876a2abea39f15be460dc3d655be6344391760a9b + +pkgname = lowpaper diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD new file mode 100644 index 0000000..f7cf99d --- /dev/null +++ b/packaging/aur/PKGBUILD @@ -0,0 +1,29 @@ +# Maintainer: Meinianda-L +pkgname=lowpaper +pkgver=0.1.1 +pkgrel=1 +pkgdesc="Lightweight video wallpaper for X11 and Wayland, made for low-end and old hardware" +arch=('any') +url="https://github.com/Meinianda-L/lowpaper" +license=('MIT') +depends=('bash' 'mpv') +optdepends=( + 'python: X11 support' + 'xorg-xprop: X11 support' + 'xorg-xrandr: X11 support' + 'xorg-xkill: X11 support' + 'xorg-xwininfo: X11 support' + 'xorg-xev: X11 monitor hotplug' + 'xorg-xset: X11 pause while the monitor is off' + 'gcc: Wayland support, and XFCE desktop icons above the video' + 'openbsd-netcat: faster control of mpv' +) +source=("lowpaper-$pkgver::https://github.com/Meinianda-L/lowpaper/releases/download/v$pkgver/lowpaper" + "LICENSE-$pkgver::https://raw.githubusercontent.com/Meinianda-L/lowpaper/v$pkgver/LICENSE") +sha256sums=('f2596b647c1d7241264712c5027a7644b6b9b0d7166f6b91691f62236c198d25' + '0a09c24d8e883bdf2a32da4876a2abea39f15be460dc3d655be6344391760a9b') + +package() { + install -Dm755 "lowpaper-$pkgver" "$pkgdir/usr/bin/lowpaper" + install -Dm644 "LICENSE-$pkgver" "$pkgdir/usr/share/licenses/$pkgname/LICENSE" +}