Add GTK wallpaper library
Scan ~/Pictures/Wallpapers and /usr/share/backgrounds/bos (configurable) and open a bread-theme GTK picker via `breadpaper library` (alias browse). Clicking a thumbnail runs the existing set path. listen honors bread.command.paper.library by spawning that picker.
This commit is contained in:
parent
01c7e5b73f
commit
1efc721990
15 changed files with 1398 additions and 31 deletions
|
|
@ -56,6 +56,7 @@ jobs:
|
||||||
sha256sum "${PKG_DIR}/breadpaper-x86_64" | awk '{print $1}' \
|
sha256sum "${PKG_DIR}/breadpaper-x86_64" | awk '{print $1}' \
|
||||||
> "${PKG_DIR}/breadpaper-x86_64.sha256"
|
> "${PKG_DIR}/breadpaper-x86_64.sha256"
|
||||||
cp src/LICENSE "${PKG_DIR}/"
|
cp src/LICENSE "${PKG_DIR}/"
|
||||||
|
cp src/packaging/breadpaper.desktop "${PKG_DIR}/"
|
||||||
cp src/bakery.toml "${PKG_DIR}/bakery.toml"
|
cp src/bakery.toml "${PKG_DIR}/bakery.toml"
|
||||||
ln -sfn "${VERSION}" "/srv/breadway-dl/dev/breadpaper/latest"
|
ln -sfn "${VERSION}" "/srv/breadway-dl/dev/breadpaper/latest"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ jobs:
|
||||||
sha256sum "${PKG_DIR}/breadpaper-x86_64" | awk '{print $1}' \
|
sha256sum "${PKG_DIR}/breadpaper-x86_64" | awk '{print $1}' \
|
||||||
> "${PKG_DIR}/breadpaper-x86_64.sha256"
|
> "${PKG_DIR}/breadpaper-x86_64.sha256"
|
||||||
cp src/LICENSE "${PKG_DIR}/"
|
cp src/LICENSE "${PKG_DIR}/"
|
||||||
|
cp src/packaging/breadpaper.desktop "${PKG_DIR}/"
|
||||||
cp src/bakery.toml "${PKG_DIR}/bakery.toml"
|
cp src/bakery.toml "${PKG_DIR}/bakery.toml"
|
||||||
ln -sfn "${VERSION}" "/srv/breadway-dl/beta/breadpaper/latest"
|
ln -sfn "${VERSION}" "/srv/breadway-dl/beta/breadpaper/latest"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ jobs:
|
||||||
sha256sum "${PKG_DIR}/breadpaper-x86_64" | awk '{print $1}' \
|
sha256sum "${PKG_DIR}/breadpaper-x86_64" | awk '{print $1}' \
|
||||||
> "${PKG_DIR}/breadpaper-x86_64.sha256"
|
> "${PKG_DIR}/breadpaper-x86_64.sha256"
|
||||||
cp LICENSE "${PKG_DIR}/"
|
cp LICENSE "${PKG_DIR}/"
|
||||||
|
cp packaging/breadpaper.desktop "${PKG_DIR}/"
|
||||||
cp bakery.toml "${PKG_DIR}/bakery.toml"
|
cp bakery.toml "${PKG_DIR}/bakery.toml"
|
||||||
ln -sfn "${VERSION}" "${DL_DIR}/breadpaper/latest"
|
ln -sfn "${VERSION}" "${DL_DIR}/breadpaper/latest"
|
||||||
|
|
||||||
|
|
|
||||||
565
Cargo.lock
generated
565
Cargo.lock
generated
|
|
@ -58,6 +58,18 @@ version = "1.0.104"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bread-shared"
|
name = "bread-shared"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
|
|
@ -66,7 +78,18 @@ dependencies = [
|
||||||
"dirs",
|
"dirs",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"toml",
|
"toml 0.8.23",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bread-theme"
|
||||||
|
version = "0.7.2"
|
||||||
|
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517f161724132cdeb658c04cf5e490be07ee73"
|
||||||
|
dependencies = [
|
||||||
|
"dirs",
|
||||||
|
"gtk4",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -85,9 +108,46 @@ name = "breadpaper"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"bread-theme",
|
||||||
"bread-utils",
|
"bread-utils",
|
||||||
"clap",
|
"clap",
|
||||||
|
"gtk4",
|
||||||
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"toml 0.8.23",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cairo-rs"
|
||||||
|
version = "0.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5cc8d9aa793480744cd9a0524fef1a2e197d9eaa0f739cde19d16aba530dcb95"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cairo-sys-rs",
|
||||||
|
"glib",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cairo-sys-rs"
|
||||||
|
version = "0.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8b4985713047f5faee02b8db6a6ef32bbb50269ff53c1aee716d1d195b76d54"
|
||||||
|
dependencies = [
|
||||||
|
"glib-sys",
|
||||||
|
"libc",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-expr"
|
||||||
|
version = "0.20.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c"
|
||||||
|
dependencies = [
|
||||||
|
"smallvec",
|
||||||
|
"target-lexicon",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -169,6 +229,135 @@ version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "field-offset"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
|
||||||
|
dependencies = [
|
||||||
|
"memoffset",
|
||||||
|
"rustc_version",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-channel"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-executor"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-io"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-macro"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-macro",
|
||||||
|
"futures-task",
|
||||||
|
"pin-project-lite",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gdk-pixbuf"
|
||||||
|
version = "0.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "25f420376dbee041b2db374ce4573892a36222bb3f6c0c43e24f0d67eae9b646"
|
||||||
|
dependencies = [
|
||||||
|
"gdk-pixbuf-sys",
|
||||||
|
"gio",
|
||||||
|
"glib",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gdk-pixbuf-sys"
|
||||||
|
version = "0.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48f31b37b1fc4b48b54f6b91b7ef04c18e00b4585d98359dd7b998774bbd91fb"
|
||||||
|
dependencies = [
|
||||||
|
"gio-sys",
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"libc",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gdk4"
|
||||||
|
version = "0.11.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d81e2a6c6ecba2aab60633a98df1868b03fa0bfdce8105edc27c1bccf71f0e39"
|
||||||
|
dependencies = [
|
||||||
|
"cairo-rs",
|
||||||
|
"gdk-pixbuf",
|
||||||
|
"gdk4-sys",
|
||||||
|
"gio",
|
||||||
|
"glib",
|
||||||
|
"libc",
|
||||||
|
"pango",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gdk4-sys"
|
||||||
|
version = "0.11.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d8f608d8d7d229975c4d0d026f5d3071598c4ddab3c5262b0a31840fec78d13"
|
||||||
|
dependencies = [
|
||||||
|
"cairo-sys-rs",
|
||||||
|
"gdk-pixbuf-sys",
|
||||||
|
"gio-sys",
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"libc",
|
||||||
|
"pango-sys",
|
||||||
|
"pkg-config",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
|
|
@ -180,6 +369,194 @@ dependencies = [
|
||||||
"wasi",
|
"wasi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gio"
|
||||||
|
version = "0.22.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b3e1f669909c326b9413bde5a742097b8c90a7d78f45326db13668984769ded"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"futures-util",
|
||||||
|
"gio-sys",
|
||||||
|
"glib",
|
||||||
|
"libc",
|
||||||
|
"pin-project-lite",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gio-sys"
|
||||||
|
version = "0.22.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "353fdc7da7cd16da916104b1e0e4e7de380ec9c8aaa20d4d742d66310ab4b0d5"
|
||||||
|
dependencies = [
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"libc",
|
||||||
|
"system-deps",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glib"
|
||||||
|
version = "0.22.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ddbcf514bd1881fc1b960e4e52b4e82873f4da3bceddbd58d42827b508888100"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-executor",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
"gio-sys",
|
||||||
|
"glib-macros",
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"libc",
|
||||||
|
"memchr",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glib-macros"
|
||||||
|
version = "0.22.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "506d23499707c7142898429757e8d9a3871d965239a2cb66dfa05052be6d6f19"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glib-sys"
|
||||||
|
version = "0.22.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "030967459f9f676851872c6304adea7825c6d462ec9b72554c733cf0c5952233"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gobject-sys"
|
||||||
|
version = "0.22.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22a861859b887a79cf461359c192c97a57d8fb0229dd291232e57aa11f6fa72c"
|
||||||
|
dependencies = [
|
||||||
|
"glib-sys",
|
||||||
|
"libc",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "graphene-rs"
|
||||||
|
version = "0.22.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eb856b9c558971c3f13ab692358926da710b046932a4e087aedcc35b040d7dff"
|
||||||
|
dependencies = [
|
||||||
|
"glib",
|
||||||
|
"graphene-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "graphene-sys"
|
||||||
|
version = "0.22.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c7ffdfde88f3570d3705e0d8a2433e036d387a1f2930bbf47eafcb5f569fd04"
|
||||||
|
dependencies = [
|
||||||
|
"glib-sys",
|
||||||
|
"libc",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gsk4"
|
||||||
|
version = "0.11.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b867be1c5f14dcb8f552c0eff6e9a9b1da5f8b43943e8efc3a63c889d84952ff"
|
||||||
|
dependencies = [
|
||||||
|
"cairo-rs",
|
||||||
|
"gdk4",
|
||||||
|
"glib",
|
||||||
|
"graphene-rs",
|
||||||
|
"gsk4-sys",
|
||||||
|
"libc",
|
||||||
|
"pango",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gsk4-sys"
|
||||||
|
version = "0.11.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b7c7eb2e681ee896646cfb8872b431f24d09f53ba9283289d9b10caa6707088"
|
||||||
|
dependencies = [
|
||||||
|
"cairo-sys-rs",
|
||||||
|
"gdk4-sys",
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"graphene-sys",
|
||||||
|
"libc",
|
||||||
|
"pango-sys",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gtk4"
|
||||||
|
version = "0.11.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "98a0a0466484f64b07b5b8184d43fa46be78eb0b8e04ae4e179af31d770b76d9"
|
||||||
|
dependencies = [
|
||||||
|
"cairo-rs",
|
||||||
|
"field-offset",
|
||||||
|
"futures-channel",
|
||||||
|
"gdk-pixbuf",
|
||||||
|
"gdk4",
|
||||||
|
"gio",
|
||||||
|
"glib",
|
||||||
|
"graphene-rs",
|
||||||
|
"gsk4",
|
||||||
|
"gtk4-macros",
|
||||||
|
"gtk4-sys",
|
||||||
|
"libc",
|
||||||
|
"pango",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gtk4-macros"
|
||||||
|
version = "0.11.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5ac7179400a36a04de039c24206bb841c5596992b907b43b23ee8d5bdc40d00e"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-crate",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gtk4-sys"
|
||||||
|
version = "0.11.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "82b8f954786af0b1984425c4446b77f5ff6594346181316be3f850caab1c6f01"
|
||||||
|
dependencies = [
|
||||||
|
"cairo-sys-rs",
|
||||||
|
"gdk-pixbuf-sys",
|
||||||
|
"gdk4-sys",
|
||||||
|
"gio-sys",
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"graphene-sys",
|
||||||
|
"gsk4-sys",
|
||||||
|
"libc",
|
||||||
|
"pango-sys",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.17.1"
|
version = "0.17.1"
|
||||||
|
|
@ -235,6 +612,15 @@ version = "2.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memoffset"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell_polyfill"
|
name = "once_cell_polyfill"
|
||||||
version = "1.70.2"
|
version = "1.70.2"
|
||||||
|
|
@ -247,6 +633,50 @@ version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pango"
|
||||||
|
version = "0.22.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d800d8d0de2ad5d0fb046f5344dbaba14a003cf3dd27cc21d85893d35ea316c"
|
||||||
|
dependencies = [
|
||||||
|
"gio",
|
||||||
|
"glib",
|
||||||
|
"pango-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pango-sys"
|
||||||
|
version = "0.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbd111a20ca90fedf03e09c59783c679c00900f1d8491cca5399f5e33609d5d6"
|
||||||
|
dependencies = [
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"libc",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkg-config"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-crate"
|
||||||
|
version = "3.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||||
|
dependencies = [
|
||||||
|
"toml_edit 0.25.13+spec-1.1.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.107"
|
version = "1.0.107"
|
||||||
|
|
@ -276,6 +706,21 @@ dependencies = [
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc_version"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||||
|
dependencies = [
|
||||||
|
"semver",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "semver"
|
||||||
|
version = "1.0.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.229"
|
version = "1.0.229"
|
||||||
|
|
@ -328,6 +773,27 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.15.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
|
|
@ -356,6 +822,25 @@ dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "system-deps"
|
||||||
|
version = "7.0.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-expr",
|
||||||
|
"heck",
|
||||||
|
"pkg-config",
|
||||||
|
"toml 1.1.4+spec-1.1.0",
|
||||||
|
"version-compare",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "target-lexicon"
|
||||||
|
version = "0.13.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.69"
|
version = "1.0.69"
|
||||||
|
|
@ -383,9 +868,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned 0.6.9",
|
||||||
"toml_datetime",
|
"toml_datetime 0.6.11",
|
||||||
"toml_edit",
|
"toml_edit 0.22.27",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "1.1.4+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde_core",
|
||||||
|
"serde_spanned 1.1.1",
|
||||||
|
"toml_datetime 1.1.1+spec-1.1.0",
|
||||||
|
"toml_parser",
|
||||||
|
"toml_writer",
|
||||||
|
"winnow 1.0.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -397,6 +897,15 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "1.1.1+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.22.27"
|
version = "0.22.27"
|
||||||
|
|
@ -405,10 +914,31 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned 0.6.9",
|
||||||
"toml_datetime",
|
"toml_datetime 0.6.11",
|
||||||
"toml_write",
|
"toml_write",
|
||||||
"winnow",
|
"winnow 0.7.15",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.25.13+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"toml_datetime 1.1.1+spec-1.1.0",
|
||||||
|
"toml_parser",
|
||||||
|
"winnow 1.0.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_parser"
|
||||||
|
version = "1.1.3+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
|
||||||
|
dependencies = [
|
||||||
|
"winnow 1.0.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -417,6 +947,12 @@ version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_writer"
|
||||||
|
version = "1.1.2+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
|
|
@ -429,6 +965,12 @@ version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version-compare"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.1+wasi-snapshot-preview1"
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
|
@ -525,6 +1067,15 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.23"
|
version = "1.0.23"
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,9 @@ license = "MIT"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
toml = "0.8"
|
||||||
|
gtk4 = { version = "0.11", features = ["v4_12"] }
|
||||||
|
bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["gtk"] }
|
||||||
bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["bread-client"] }
|
bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["bread-client"] }
|
||||||
|
|
|
||||||
18
EVENTS.md
18
EVENTS.md
|
|
@ -4,7 +4,7 @@ breadpaper is a wallpaper setter: it works exactly the same with or
|
||||||
without `breadd` running. When breadd *is* present, a successful
|
without `breadd` running. When breadd *is* present, a successful
|
||||||
`breadpaper set` (or the bare-path shorthand) publishes `bread.paper.changed`
|
`breadpaper set` (or the bare-path shorthand) publishes `bread.paper.changed`
|
||||||
into the shared bread automation fabric, and `breadpaper listen` honors
|
into the shared bread automation fabric, and `breadpaper listen` honors
|
||||||
`bread.command.paper.set`. See the parent `bread` repo's
|
`bread.command.paper.set` and `bread.command.paper.library`. See the parent `bread` repo's
|
||||||
`Documentation.md` — specifically its "Namespaces" and "Integrating a
|
`Documentation.md` — specifically its "Namespaces" and "Integrating a
|
||||||
bread\* app" sections — for the general convention this follows.
|
bread\* app" sections — for the general convention this follows.
|
||||||
|
|
||||||
|
|
@ -15,9 +15,9 @@ long-running `listen` subcommand holds a `subscribe` open.
|
||||||
|
|
||||||
`breadpaper listen` is fail-silent if breadd is down: `subscribe`
|
`breadpaper listen` is fail-silent if breadd is down: `subscribe`
|
||||||
reconnects with backoff and simply delivers nothing until the daemon
|
reconnects with backoff and simply delivers nothing until the daemon
|
||||||
comes back. The one-shot `set`/`get` path does not require `listen`.
|
comes back. The one-shot `set`/`get`/`library` path does not require
|
||||||
Modules that want to change the wallpaper without a listener can still
|
`listen`. Modules that want to change the wallpaper without a listener
|
||||||
shell out:
|
can still shell out:
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
bread.exec("breadpaper set /path/to/image.png")
|
bread.exec("breadpaper set /path/to/image.png")
|
||||||
|
|
@ -38,6 +38,8 @@ bread.wait("bread.paper.set.done", { timeout = 10000 })
|
||||||
| `bread.paper.changed` | `{ "path": "<wallpaper>" }` | After a successful `set` (awww + wal + `bread-theme reload`), including when `listen` honors `bread.command.paper.set`. `path` is the canonical absolute path that was applied. Not emitted on `get`, and not emitted if any of the three steps fail. |
|
| `bread.paper.changed` | `{ "path": "<wallpaper>" }` | After a successful `set` (awww + wal + `bread-theme reload`), including when `listen` honors `bread.command.paper.set`. `path` is the canonical absolute path that was applied. Not emitted on `get`, and not emitted if any of the three steps fail. |
|
||||||
| `bread.paper.set.done` | `{ "path": "<wallpaper>" }` | `bread.command.paper.set` was received and `set()` succeeded. `path` is the canonical absolute path that was applied. Not emitted by the one-shot CLI `set` — that path only publishes `changed`. |
|
| `bread.paper.set.done` | `{ "path": "<wallpaper>" }` | `bread.command.paper.set` was received and `set()` succeeded. `path` is the canonical absolute path that was applied. Not emitted by the one-shot CLI `set` — that path only publishes `changed`. |
|
||||||
| `bread.paper.set.failed` | `{ "error": "<message>", "path"?: "<requested>" }` | `bread.command.paper.set` was received but `set()` failed, or `data.path` was missing/not a string. `path` is the requested (not canonical) path when one was supplied. |
|
| `bread.paper.set.failed` | `{ "error": "<message>", "path"?: "<requested>" }` | `bread.command.paper.set` was received but `set()` failed, or `data.path` was missing/not a string. `path` is the requested (not canonical) path when one was supplied. |
|
||||||
|
| `bread.paper.library.done` | `{}` | `bread.command.paper.library` was received and a `breadpaper library` process was started. Not emitted by the one-shot CLI `library` / `browse`. |
|
||||||
|
| `bread.paper.library.failed` | `{ "error": "<message>" }` | `bread.command.paper.library` was received but the library process could not be spawned. |
|
||||||
|
|
||||||
## Commands honored (`bread.command.paper.*`)
|
## Commands honored (`bread.command.paper.*`)
|
||||||
|
|
||||||
|
|
@ -48,13 +50,13 @@ Honored only while `breadpaper listen` is running. A
|
||||||
| Verb | Data | Effect |
|
| Verb | Data | Effect |
|
||||||
|------|------|--------|
|
|------|------|--------|
|
||||||
| `set` | `{ "path": "..." }` | Calls the existing `set()` (awww + wal + `bread-theme reload`). Emits `bread.paper.set.done` / `.failed`. A successful set also emits `bread.paper.changed`. |
|
| `set` | `{ "path": "..." }` | Calls the existing `set()` (awww + wal + `bread-theme reload`). Emits `bread.paper.set.done` / `.failed`. A successful set also emits `bread.paper.changed`. |
|
||||||
|
| `library` | `{}` | Spawns `breadpaper library` (GTK picker). Emits `bread.paper.library.done` once the process is started, or `bread.paper.library.failed` if the spawn fails. Clicking a thumbnail in that window is a normal `set` and publishes `bread.paper.changed`. |
|
||||||
|
|
||||||
### Not implemented: slideshow / library / random / next
|
### Not implemented: slideshow / random / next
|
||||||
|
|
||||||
breadpaper is not a wallpaper library, a slideshow daemon, or a picker.
|
`breadpaper library` / `browse` is the in-app picker. Do not invent
|
||||||
Browsing `~/Pictures/Backgrounds` lives in bos-settings. Do not invent
|
|
||||||
`bread.command.paper.next` / `.random` / `.cycle` (or matching events)
|
`bread.command.paper.next` / `.random` / `.cycle` (or matching events)
|
||||||
ahead of a real product feature. Unrecognized verbs are ignored.
|
ahead of a real slideshow feature. Unrecognized verbs are ignored.
|
||||||
|
|
||||||
## Fail-safe behavior
|
## Fail-safe behavior
|
||||||
|
|
||||||
|
|
|
||||||
40
README.md
40
README.md
|
|
@ -5,9 +5,9 @@ via [awww](https://github.com/heywoodlh/awww), generates a palette with
|
||||||
[pywal](https://github.com/dylanaraps/pywal) (`wal`), and runs
|
[pywal](https://github.com/dylanaraps/pywal) (`wal`), and runs
|
||||||
`bread-theme reload`.
|
`bread-theme reload`.
|
||||||
|
|
||||||
It is not a wallpaper library, a slideshow daemon, or a GUI. Browsing
|
`set` / `get` stay one-shot CLI. `breadpaper library` (alias `browse`)
|
||||||
`~/Pictures/Backgrounds` and picking an image lives in
|
opens a GTK picker over the wallpaper directories. It is not a slideshow
|
||||||
[bos-settings](https://git.breadway.dev/Breadway/bos-settings).
|
daemon.
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
|
|
@ -17,7 +17,8 @@ Must be on `$PATH` for `set`:
|
||||||
- `wal` — palette generation (`python-pywal`)
|
- `wal` — palette generation (`python-pywal`)
|
||||||
- `bread-theme` — theme reload (bakery package, not `breadd`)
|
- `bread-theme` — theme reload (bakery package, not `breadd`)
|
||||||
|
|
||||||
`get` only reads the path pywal stored at `~/.cache/wal/wal`.
|
`library` also needs GTK4 (the window loads `bread-theme`'s shared
|
||||||
|
stylesheet). `get` only reads the path pywal stored at `~/.cache/wal/wal`.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
|
@ -38,18 +39,41 @@ install -Dm755 target/release/breadpaper ~/.local/bin/breadpaper
|
||||||
breadpaper <path> # shorthand for `set`
|
breadpaper <path> # shorthand for `set`
|
||||||
breadpaper set <path> # awww + wal + bread-theme reload
|
breadpaper set <path> # awww + wal + bread-theme reload
|
||||||
breadpaper get # print the current wallpaper path
|
breadpaper get # print the current wallpaper path
|
||||||
breadpaper listen # honor bread.command.paper.set (fail-silent if breadd is down)
|
breadpaper library # GTK picker (alias: browse)
|
||||||
|
breadpaper library --dir PATH # also scan PATH (repeatable)
|
||||||
|
breadpaper listen # honor bread.command.paper.set / .library
|
||||||
```
|
```
|
||||||
|
|
||||||
Supported formats: `png`, `jpg`, `jpeg`, `webp`, `gif`, `bmp`.
|
Supported formats: `png`, `jpg`, `jpeg`, `webp`, `gif`, `bmp`.
|
||||||
|
|
||||||
|
## Library
|
||||||
|
|
||||||
|
`breadpaper library` scans these directories (missing ones are skipped):
|
||||||
|
|
||||||
|
1. `~/Pictures/Wallpapers`
|
||||||
|
2. `/usr/share/backgrounds/bos`
|
||||||
|
|
||||||
|
Override the list in `~/.config/breadpaper/config.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
library_dirs = [
|
||||||
|
"~/Pictures/Wallpapers",
|
||||||
|
"/usr/share/backgrounds/bos",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
`BREADPAPER_LIBRARY_DIRS` (colon-separated) overrides the file. `--dir`
|
||||||
|
appends extra roots for that invocation. Clicking a thumbnail runs the
|
||||||
|
same `set` path as the CLI.
|
||||||
|
|
||||||
## Bread events
|
## Bread events
|
||||||
|
|
||||||
After a successful `set`, breadpaper emits `bread.paper.changed` if
|
After a successful `set`, breadpaper emits `bread.paper.changed` if
|
||||||
`breadd` is running (silent no-op if it isn't). `breadpaper listen` is
|
`breadd` is running (silent no-op if it isn't). `breadpaper listen` is
|
||||||
the optional long-running subscriber for `bread.command.paper.set`; it
|
the optional long-running subscriber for `bread.command.paper.set` and
|
||||||
does not start by itself. Lua modules can still
|
`bread.command.paper.library`; it does not start by itself. Lua modules
|
||||||
`bread.exec("breadpaper set …")`. See [EVENTS.md](EVENTS.md).
|
can still `bread.exec("breadpaper set …")` or
|
||||||
|
`bread.exec("breadpaper library")`. See [EVENTS.md](EVENTS.md).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
name = "breadpaper"
|
name = "breadpaper"
|
||||||
description = "Wallpaper manager for the bread desktop — sets awww wallpaper, generates pywal palette, reloads bread themes"
|
description = "Wallpaper manager for the bread desktop — sets awww wallpaper, generates pywal palette, reloads bread themes"
|
||||||
binaries = ["breadpaper"]
|
binaries = ["breadpaper"]
|
||||||
system_deps = ["awww", "python-pywal"]
|
system_deps = ["awww", "python-pywal", "gtk4"]
|
||||||
optional_system_deps = []
|
optional_system_deps = []
|
||||||
bread_deps = ["bread-theme"]
|
bread_deps = ["bread-theme"]
|
||||||
license_file = "LICENSE"
|
license_file = "LICENSE"
|
||||||
|
desktop_file = "breadpaper.desktop"
|
||||||
|
|
||||||
[install]
|
[install]
|
||||||
post_install = []
|
post_install = []
|
||||||
|
|
|
||||||
6
config.example.toml
Normal file
6
config.example.toml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# ~/.config/breadpaper/config.toml
|
||||||
|
# Missing directories are skipped. An empty list uses the built-in defaults.
|
||||||
|
library_dirs = [
|
||||||
|
"~/Pictures/Wallpapers",
|
||||||
|
"/usr/share/backgrounds/bos",
|
||||||
|
]
|
||||||
9
packaging/breadpaper.desktop
Normal file
9
packaging/breadpaper.desktop
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=Wallpapers
|
||||||
|
Comment=Browse and apply wallpapers on the Bread desktop
|
||||||
|
Exec=breadpaper library
|
||||||
|
Icon=preferences-desktop-wallpaper
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Categories=Settings;DesktopSettings;
|
||||||
|
StartupWMClass=com.breadway.breadpaper
|
||||||
252
src/config.rs
Normal file
252
src/config.rs
Normal file
|
|
@ -0,0 +1,252 @@
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
/// User library directory used when no config file is present.
|
||||||
|
pub const DEFAULT_USER_LIBRARY: &str = "Pictures/Wallpapers";
|
||||||
|
|
||||||
|
/// Packaged BOS backgrounds, scanned when the directory exists.
|
||||||
|
pub const DEFAULT_SYSTEM_LIBRARY: &str = "/usr/share/backgrounds/bos";
|
||||||
|
|
||||||
|
/// Colon-separated override of [`Config::library_dirs`]. Empty means "use
|
||||||
|
/// the config file / defaults".
|
||||||
|
pub const LIBRARY_DIRS_ENV: &str = "BREADPAPER_LIBRARY_DIRS";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Config {
|
||||||
|
pub library_dirs: Vec<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
struct ConfigFile {
|
||||||
|
#[serde(default)]
|
||||||
|
library_dirs: Vec<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
library_dirs: default_library_dirs(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn path() -> PathBuf {
|
||||||
|
bread_utils::xdg::config_dir("breadpaper").join("config.toml")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load() -> Self {
|
||||||
|
Self::load_from(&Self::path())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_from(path: &Path) -> Self {
|
||||||
|
let mut cfg = match std::fs::read_to_string(path) {
|
||||||
|
Ok(text) => match toml::from_str::<ConfigFile>(&text) {
|
||||||
|
Ok(parsed) if !parsed.library_dirs.is_empty() => Self {
|
||||||
|
library_dirs: parsed.library_dirs,
|
||||||
|
},
|
||||||
|
Ok(_) => Self::default(),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(
|
||||||
|
"breadpaper: {} failed to parse ({e}); using defaults",
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => Self::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(dirs) = env_library_dirs() {
|
||||||
|
cfg.library_dirs = dirs;
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.library_dirs = cfg
|
||||||
|
.library_dirs
|
||||||
|
.into_iter()
|
||||||
|
.map(expand_tilde)
|
||||||
|
.filter(|p| !p.as_os_str().is_empty())
|
||||||
|
.collect();
|
||||||
|
cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_extra_dirs(mut self, extra: impl IntoIterator<Item = PathBuf>) -> Self {
|
||||||
|
self.library_dirs
|
||||||
|
.extend(extra.into_iter().map(expand_tilde));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_library_dirs() -> Vec<PathBuf> {
|
||||||
|
vec![
|
||||||
|
bread_utils::xdg::home_dir().join(DEFAULT_USER_LIBRARY),
|
||||||
|
PathBuf::from(DEFAULT_SYSTEM_LIBRARY),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expand_tilde(path: PathBuf) -> PathBuf {
|
||||||
|
let Some(s) = path.to_str() else {
|
||||||
|
return path;
|
||||||
|
};
|
||||||
|
if s == "~" {
|
||||||
|
return bread_utils::xdg::home_dir();
|
||||||
|
}
|
||||||
|
if let Some(rest) = s.strip_prefix("~/") {
|
||||||
|
return bread_utils::xdg::home_dir().join(rest);
|
||||||
|
}
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env_library_dirs() -> Option<Vec<PathBuf>> {
|
||||||
|
let raw = std::env::var(LIBRARY_DIRS_ENV).ok()?;
|
||||||
|
if raw.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let dirs: Vec<PathBuf> = raw
|
||||||
|
.split(':')
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.collect();
|
||||||
|
if dirs.is_empty() { None } else { Some(dirs) }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
|
||||||
|
fn env_lock() -> std::sync::MutexGuard<'static, ()> {
|
||||||
|
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||||
|
LOCK.get_or_init(|| Mutex::new(()))
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|e| e.into_inner())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tmp_dir(name: &str) -> PathBuf {
|
||||||
|
let dir = std::env::temp_dir().join(format!(
|
||||||
|
"breadpaper-config-{name}-{}-{}",
|
||||||
|
std::process::id(),
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_nanos())
|
||||||
|
.unwrap_or(0)
|
||||||
|
));
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
dir
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_dirs_are_pictures_wallpapers_and_bos_backgrounds() {
|
||||||
|
let dirs = Config::default().library_dirs;
|
||||||
|
assert!(
|
||||||
|
dirs.iter().any(|d| d.ends_with(DEFAULT_USER_LIBRARY)),
|
||||||
|
"missing ~/{DEFAULT_USER_LIBRARY} in {dirs:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
dirs.iter().any(|d| d == Path::new(DEFAULT_SYSTEM_LIBRARY)),
|
||||||
|
"missing {DEFAULT_SYSTEM_LIBRARY} in {dirs:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expand_tilde_prefix() {
|
||||||
|
let home = bread_utils::xdg::home_dir();
|
||||||
|
assert_eq!(
|
||||||
|
expand_tilde(PathBuf::from("~/Pictures/Wallpapers")),
|
||||||
|
home.join("Pictures/Wallpapers")
|
||||||
|
);
|
||||||
|
assert_eq!(expand_tilde(PathBuf::from("~")), home);
|
||||||
|
let abs = PathBuf::from("/usr/share/backgrounds/bos");
|
||||||
|
assert_eq!(expand_tilde(abs.clone()), abs);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_from_missing_file_uses_defaults() {
|
||||||
|
let _lock = env_lock();
|
||||||
|
let prev = std::env::var_os(LIBRARY_DIRS_ENV);
|
||||||
|
unsafe { std::env::remove_var(LIBRARY_DIRS_ENV) };
|
||||||
|
let cfg = Config::load_from(&PathBuf::from("/no/such/breadpaper-config.toml"));
|
||||||
|
if let Some(v) = prev {
|
||||||
|
unsafe { std::env::set_var(LIBRARY_DIRS_ENV, v) };
|
||||||
|
}
|
||||||
|
assert_eq!(cfg.library_dirs, default_library_dirs());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_from_parses_library_dirs_and_expands_tilde() {
|
||||||
|
let _lock = env_lock();
|
||||||
|
let prev = std::env::var_os(LIBRARY_DIRS_ENV);
|
||||||
|
unsafe { std::env::remove_var(LIBRARY_DIRS_ENV) };
|
||||||
|
|
||||||
|
let dir = tmp_dir("parse");
|
||||||
|
let path = dir.join("config.toml");
|
||||||
|
std::fs::write(
|
||||||
|
&path,
|
||||||
|
"library_dirs = [\"~/custom/walls\", \"/opt/walls\"]\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let cfg = Config::load_from(&path);
|
||||||
|
if let Some(v) = prev {
|
||||||
|
unsafe { std::env::set_var(LIBRARY_DIRS_ENV, v) };
|
||||||
|
}
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
assert_eq!(
|
||||||
|
cfg.library_dirs,
|
||||||
|
vec![
|
||||||
|
bread_utils::xdg::home_dir().join("custom/walls"),
|
||||||
|
PathBuf::from("/opt/walls"),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_library_dirs_key_falls_back_to_defaults() {
|
||||||
|
let _lock = env_lock();
|
||||||
|
let prev = std::env::var_os(LIBRARY_DIRS_ENV);
|
||||||
|
unsafe { std::env::remove_var(LIBRARY_DIRS_ENV) };
|
||||||
|
|
||||||
|
let dir = tmp_dir("empty");
|
||||||
|
let path = dir.join("config.toml");
|
||||||
|
std::fs::write(&path, "library_dirs = []\n").unwrap();
|
||||||
|
let cfg = Config::load_from(&path);
|
||||||
|
if let Some(v) = prev {
|
||||||
|
unsafe { std::env::set_var(LIBRARY_DIRS_ENV, v) };
|
||||||
|
}
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
assert_eq!(cfg.library_dirs, default_library_dirs());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn env_overrides_config_file() {
|
||||||
|
let _lock = env_lock();
|
||||||
|
let prev = std::env::var_os(LIBRARY_DIRS_ENV);
|
||||||
|
unsafe { std::env::set_var(LIBRARY_DIRS_ENV, "/tmp/a:/tmp/b") };
|
||||||
|
|
||||||
|
let dir = tmp_dir("env");
|
||||||
|
let path = dir.join("config.toml");
|
||||||
|
std::fs::write(&path, "library_dirs = [\"/from/file\"]\n").unwrap();
|
||||||
|
let cfg = Config::load_from(&path);
|
||||||
|
match prev {
|
||||||
|
Some(v) => unsafe { std::env::set_var(LIBRARY_DIRS_ENV, v) },
|
||||||
|
None => unsafe { std::env::remove_var(LIBRARY_DIRS_ENV) },
|
||||||
|
}
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
assert_eq!(
|
||||||
|
cfg.library_dirs,
|
||||||
|
vec![PathBuf::from("/tmp/a"), PathBuf::from("/tmp/b")]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn with_extra_dirs_appends() {
|
||||||
|
let cfg = Config {
|
||||||
|
library_dirs: vec![PathBuf::from("/a")],
|
||||||
|
}
|
||||||
|
.with_extra_dirs([PathBuf::from("/b")]);
|
||||||
|
assert_eq!(
|
||||||
|
cfg.library_dirs,
|
||||||
|
vec![PathBuf::from("/a"), PathBuf::from("/b")]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
69
src/lib.rs
69
src/lib.rs
|
|
@ -1,13 +1,21 @@
|
||||||
|
mod config;
|
||||||
|
mod library;
|
||||||
mod pywal;
|
mod pywal;
|
||||||
mod theme;
|
mod theme;
|
||||||
|
mod ui;
|
||||||
mod wallpaper;
|
mod wallpaper;
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
#[cfg(not(test))]
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{Context, Result, bail};
|
||||||
use bread_utils::bread_client::{BreadClient, BreadEvent};
|
use bread_utils::bread_client::{BreadClient, BreadEvent};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
|
pub use config::{Config, DEFAULT_SYSTEM_LIBRARY, DEFAULT_USER_LIBRARY};
|
||||||
|
pub use library::{Wallpaper, scan};
|
||||||
|
|
||||||
/// App id in bread's sibling-app registry (`KNOWN_APPS`). Events publish as
|
/// App id in bread's sibling-app registry (`KNOWN_APPS`). Events publish as
|
||||||
/// `bread.paper.*`. See `EVENTS.md`.
|
/// `bread.paper.*`. See `EVENTS.md`.
|
||||||
|
|
@ -15,6 +23,13 @@ const APP_ID: &str = "paper";
|
||||||
|
|
||||||
const IMAGE_EXTENSIONS: &[&str] = &["png", "jpg", "jpeg", "webp", "gif", "bmp"];
|
const IMAGE_EXTENSIONS: &[&str] = &["png", "jpg", "jpeg", "webp", "gif", "bmp"];
|
||||||
|
|
||||||
|
/// Open the GTK wallpaper library. Extra dirs are appended to the configured
|
||||||
|
/// scan list (`~/.config/breadpaper/config.toml`, then defaults).
|
||||||
|
pub fn library(extra_dirs: impl IntoIterator<Item = PathBuf>) -> Result<()> {
|
||||||
|
let cfg = Config::load().with_extra_dirs(extra_dirs);
|
||||||
|
ui::run(cfg.library_dirs)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set(path: &Path) -> Result<()> {
|
pub fn set(path: &Path) -> Result<()> {
|
||||||
let path = validate(path)?;
|
let path = validate(path)?;
|
||||||
apply_wallpaper(&path)?;
|
apply_wallpaper(&path)?;
|
||||||
|
|
@ -49,6 +64,7 @@ fn handle_command(event: BreadEvent) {
|
||||||
};
|
};
|
||||||
match verb {
|
match verb {
|
||||||
"set" => handle_set(&event.data),
|
"set" => handle_set(&event.data),
|
||||||
|
"library" => handle_library(),
|
||||||
other => {
|
other => {
|
||||||
eprintln!("breadpaper: ignoring unrecognized command verb '{other}'");
|
eprintln!("breadpaper: ignoring unrecognized command verb '{other}'");
|
||||||
}
|
}
|
||||||
|
|
@ -83,6 +99,46 @@ fn handle_set(data: &Value) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_library() {
|
||||||
|
let client = BreadClient::connect(APP_ID);
|
||||||
|
match open_library() {
|
||||||
|
Ok(()) => client.emit("bread.paper.library.done", json!({})),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("breadpaper: bread.command.paper.library failed: {e:#}");
|
||||||
|
client.emit(
|
||||||
|
"bread.paper.library.failed",
|
||||||
|
json!({ "error": format!("{e:#}") }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawn a one-shot `breadpaper library` so the listen loop can stay a
|
||||||
|
/// park() thread. GTK needs its own process (and argv) — mixing it into
|
||||||
|
/// `listen` would steal the main thread.
|
||||||
|
fn open_library() -> Result<()> {
|
||||||
|
spawn_library()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
fn spawn_library() -> Result<()> {
|
||||||
|
let exe = std::env::current_exe().context("cannot resolve breadpaper executable")?;
|
||||||
|
Command::new(exe)
|
||||||
|
.arg("library")
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::inherit())
|
||||||
|
.spawn()
|
||||||
|
.context("failed to spawn breadpaper library")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn spawn_library() -> Result<()> {
|
||||||
|
// cargo test's current_exe is the test harness, not breadpaper.
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get() -> Result<PathBuf> {
|
pub fn get() -> Result<PathBuf> {
|
||||||
let home = std::env::var("HOME").context("HOME not set")?;
|
let home = std::env::var("HOME").context("HOME not set")?;
|
||||||
let wal_file = PathBuf::from(home).join(".cache/wal/wal");
|
let wal_file = PathBuf::from(home).join(".cache/wal/wal");
|
||||||
|
|
@ -173,4 +229,13 @@ mod tests {
|
||||||
handle_set(&json!({}));
|
handle_set(&json!({}));
|
||||||
handle_set(&json!({ "path": 1 }));
|
handle_set(&json!({ "path": 1 }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handle_command_library_is_silent_without_breadd() {
|
||||||
|
handle_command(BreadEvent {
|
||||||
|
event: "bread.command.paper.library".into(),
|
||||||
|
timestamp: 0,
|
||||||
|
data: json!({}),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
168
src/library.rs
Normal file
168
src/library.rs
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use crate::IMAGE_EXTENSIONS;
|
||||||
|
|
||||||
|
/// Caps how many files the picker ever lists. The library is organized in
|
||||||
|
/// subfolders (show/series), so the walk is recursive — without a bound a
|
||||||
|
/// huge Pictures tree would stall the window.
|
||||||
|
pub const MAX_LIBRARY_ITEMS: usize = 200;
|
||||||
|
pub const MAX_SCAN_DEPTH: usize = 4;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Wallpaper {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_wallpaper_file(path: &Path) -> bool {
|
||||||
|
path.extension()
|
||||||
|
.and_then(|e| e.to_str())
|
||||||
|
.map(|e| {
|
||||||
|
IMAGE_EXTENSIONS
|
||||||
|
.iter()
|
||||||
|
.any(|ext| ext.eq_ignore_ascii_case(e))
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively collect images under `dirs`. Missing directories are skipped.
|
||||||
|
/// Results are sorted by filename (case-insensitive), then full path.
|
||||||
|
pub fn scan(dirs: &[PathBuf]) -> Vec<Wallpaper> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
for dir in dirs {
|
||||||
|
if !dir.is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
walk(dir, MAX_SCAN_DEPTH, &mut out, &mut seen);
|
||||||
|
if out.len() >= MAX_LIBRARY_ITEMS {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.sort_by(|a, b| {
|
||||||
|
a.name
|
||||||
|
.to_lowercase()
|
||||||
|
.cmp(&b.name.to_lowercase())
|
||||||
|
.then_with(|| a.path.cmp(&b.path))
|
||||||
|
});
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn walk(dir: &Path, depth: usize, out: &mut Vec<Wallpaper>, seen: &mut HashSet<PathBuf>) {
|
||||||
|
if depth == 0 || out.len() >= MAX_LIBRARY_ITEMS {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Ok(entries) = std::fs::read_dir(dir) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut entries: Vec<_> = entries.flatten().collect();
|
||||||
|
entries.sort_by_key(|e| e.file_name());
|
||||||
|
for entry in entries {
|
||||||
|
if out.len() >= MAX_LIBRARY_ITEMS {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let path = entry.path();
|
||||||
|
let name = entry.file_name();
|
||||||
|
if name.to_string_lossy().starts_with('.') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if path.is_dir() {
|
||||||
|
walk(&path, depth - 1, out, seen);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !is_wallpaper_file(&path) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let canonical = path.canonicalize().unwrap_or_else(|_| path.clone());
|
||||||
|
if !seen.insert(canonical.clone()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push(Wallpaper {
|
||||||
|
name: path
|
||||||
|
.file_stem()
|
||||||
|
.or_else(|| path.file_name())
|
||||||
|
.map(|s| s.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
path: canonical,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn tmp_dir(name: &str) -> PathBuf {
|
||||||
|
let dir = std::env::temp_dir().join(format!(
|
||||||
|
"breadpaper-scan-{name}-{}-{}",
|
||||||
|
std::process::id(),
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_nanos())
|
||||||
|
.unwrap_or(0)
|
||||||
|
));
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
dir
|
||||||
|
}
|
||||||
|
|
||||||
|
fn touch(path: &Path) {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent).unwrap();
|
||||||
|
}
|
||||||
|
std::fs::write(path, []).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_wallpaper_file_accepts_known_extensions() {
|
||||||
|
assert!(is_wallpaper_file(Path::new("a.PNG")));
|
||||||
|
assert!(is_wallpaper_file(Path::new("b.jpeg")));
|
||||||
|
assert!(is_wallpaper_file(Path::new("c.webp")));
|
||||||
|
assert!(!is_wallpaper_file(Path::new("d.txt")));
|
||||||
|
assert!(!is_wallpaper_file(Path::new("noext")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scan_skips_missing_dirs() {
|
||||||
|
assert!(scan(&[PathBuf::from("/no/such/breadpaper-walls")]).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scan_finds_images_and_ignores_other_files() {
|
||||||
|
let dir = tmp_dir("find");
|
||||||
|
touch(&dir.join("keep.png"));
|
||||||
|
touch(&dir.join("notes.txt"));
|
||||||
|
touch(&dir.join(".hidden.jpg"));
|
||||||
|
touch(&dir.join("nested").join("deep.jpg"));
|
||||||
|
let found = scan(std::slice::from_ref(&dir));
|
||||||
|
let names: Vec<_> = found.iter().map(|w| w.name.as_str()).collect();
|
||||||
|
assert!(names.contains(&"keep"), "{names:?}");
|
||||||
|
assert!(names.contains(&"deep"), "{names:?}");
|
||||||
|
assert!(
|
||||||
|
!names
|
||||||
|
.iter()
|
||||||
|
.any(|n| n.contains("notes") || n.contains("hidden"))
|
||||||
|
);
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scan_dedups_the_same_file_via_two_roots() {
|
||||||
|
let dir = tmp_dir("dedup");
|
||||||
|
touch(&dir.join("one.png"));
|
||||||
|
let found = scan(&[dir.clone(), dir.clone()]);
|
||||||
|
assert_eq!(found.len(), 1);
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scan_respects_item_cap() {
|
||||||
|
let dir = tmp_dir("cap");
|
||||||
|
for i in 0..(MAX_LIBRARY_ITEMS + 10) {
|
||||||
|
touch(&dir.join(format!("{i:04}.png")));
|
||||||
|
}
|
||||||
|
let found = scan(std::slice::from_ref(&dir));
|
||||||
|
assert_eq!(found.len(), MAX_LIBRARY_ITEMS);
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/main.rs
20
src/main.rs
|
|
@ -4,7 +4,11 @@ use std::process;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "breadpaper", version, about = "Wallpaper manager for the bread desktop")]
|
#[command(
|
||||||
|
name = "breadpaper",
|
||||||
|
version,
|
||||||
|
about = "Wallpaper manager for the bread desktop"
|
||||||
|
)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
/// Image file to set as wallpaper (shorthand for `set`)
|
/// Image file to set as wallpaper (shorthand for `set`)
|
||||||
path: Option<PathBuf>,
|
path: Option<PathBuf>,
|
||||||
|
|
@ -16,13 +20,18 @@ struct Cli {
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum Command {
|
enum Command {
|
||||||
/// Set wallpaper, generate pywal palette, and reload bread themes
|
/// Set wallpaper, generate pywal palette, and reload bread themes
|
||||||
Set {
|
Set { path: PathBuf },
|
||||||
path: PathBuf,
|
|
||||||
},
|
|
||||||
/// Print the current wallpaper path
|
/// Print the current wallpaper path
|
||||||
Get,
|
Get,
|
||||||
/// Honor bread.command.paper.set until killed
|
/// Honor bread.command.paper.set / .library until killed
|
||||||
Listen,
|
Listen,
|
||||||
|
/// Open the wallpaper library (alias: browse)
|
||||||
|
#[command(visible_alias = "browse")]
|
||||||
|
Library {
|
||||||
|
/// Extra directory to scan (repeatable; added to configured dirs)
|
||||||
|
#[arg(short, long = "dir", value_name = "DIR")]
|
||||||
|
dirs: Vec<PathBuf>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
@ -31,6 +40,7 @@ fn main() {
|
||||||
let result = match (cli.command, cli.path) {
|
let result = match (cli.command, cli.path) {
|
||||||
(Some(Command::Set { path }), _) | (None, Some(path)) => breadpaper::set(&path),
|
(Some(Command::Set { path }), _) | (None, Some(path)) => breadpaper::set(&path),
|
||||||
(Some(Command::Listen), _) => breadpaper::listen(),
|
(Some(Command::Listen), _) => breadpaper::listen(),
|
||||||
|
(Some(Command::Library { dirs }), _) => breadpaper::library(dirs),
|
||||||
(Some(Command::Get), _) | (None, None) => {
|
(Some(Command::Get), _) | (None, None) => {
|
||||||
breadpaper::get().map(|p| println!("{}", p.display()))
|
breadpaper::get().map(|p| println!("{}", p.display()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
272
src/ui.rs
Normal file
272
src/ui.rs
Normal file
|
|
@ -0,0 +1,272 @@
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use gtk4::gdk_pixbuf::Pixbuf;
|
||||||
|
use gtk4::gio::ApplicationFlags;
|
||||||
|
use gtk4::prelude::*;
|
||||||
|
use gtk4::{
|
||||||
|
Align, Application, ApplicationWindow, Box as GBox, Button, ContentFit, CssProvider, FlowBox,
|
||||||
|
FlowBoxChild, HeaderBar, Label, Orientation, Picture, PolicyType, ScrolledWindow,
|
||||||
|
SelectionMode, Stack,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::library::{self, Wallpaper};
|
||||||
|
|
||||||
|
const APP_ID: &str = "com.breadway.breadpaper";
|
||||||
|
const THUMB_W: i32 = 240;
|
||||||
|
const THUMB_H: i32 = 135;
|
||||||
|
|
||||||
|
const APP_CSS: &str = "\
|
||||||
|
headerbar {\
|
||||||
|
background-color: @bg; color: @on-bg; box-shadow: none;\
|
||||||
|
border-bottom: 1px solid alpha(@on-bg, 0.08);\
|
||||||
|
}\n\
|
||||||
|
.library-chrome { padding: 12px 16px 8px 16px; }\n\
|
||||||
|
.library-grid { padding: 8px 12px 16px 12px; }\n\
|
||||||
|
.library-empty { padding: 32px 24px; }\n\
|
||||||
|
.wallpaper-tile {\
|
||||||
|
padding: 0; background-color: @surface; color: @on-surface;\
|
||||||
|
border-radius: 8px;\
|
||||||
|
}\n\
|
||||||
|
.wallpaper-tile:hover { background-color: alpha(@on-surface, 0.14); }\n\
|
||||||
|
.wallpaper-tile.current { box-shadow: inset 0 0 0 2px @accent; }\n\
|
||||||
|
.wallpaper-name { padding: 8px 10px; font-size: 12px; }\n\
|
||||||
|
";
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static APP_PROVIDER: RefCell<Option<CssProvider>> = const { RefCell::new(None) };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(dirs: Vec<PathBuf>) -> Result<()> {
|
||||||
|
let app = Application::builder()
|
||||||
|
.application_id(APP_ID)
|
||||||
|
.flags(ApplicationFlags::empty())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
app.connect_activate(move |app| present(app, dirs.clone()));
|
||||||
|
// Clap already consumed argv; do not let GApplication re-parse `library --dir`.
|
||||||
|
let _ = app.run_with_args(&["breadpaper"]);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn present(app: &Application, dirs: Vec<PathBuf>) {
|
||||||
|
bread_theme::gtk::apply_shared();
|
||||||
|
APP_PROVIDER.with(|cell| bread_theme::gtk::apply_css(APP_CSS, cell));
|
||||||
|
|
||||||
|
let window = ApplicationWindow::builder()
|
||||||
|
.application(app)
|
||||||
|
.title("Wallpapers")
|
||||||
|
.default_width(960)
|
||||||
|
.default_height(640)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let header = HeaderBar::new();
|
||||||
|
window.set_titlebar(Some(&header));
|
||||||
|
|
||||||
|
let refresh = Button::with_label("Refresh");
|
||||||
|
header.pack_end(&refresh);
|
||||||
|
|
||||||
|
let root = GBox::new(Orientation::Vertical, 0);
|
||||||
|
|
||||||
|
let chrome = GBox::new(Orientation::Vertical, 6);
|
||||||
|
chrome.add_css_class("library-chrome");
|
||||||
|
|
||||||
|
let summary = Label::new(None);
|
||||||
|
summary.set_xalign(0.0);
|
||||||
|
summary.set_wrap(true);
|
||||||
|
summary.add_css_class("dim");
|
||||||
|
chrome.append(&summary);
|
||||||
|
|
||||||
|
let status = Label::new(Some("Click a wallpaper to apply it."));
|
||||||
|
status.set_xalign(0.0);
|
||||||
|
status.set_wrap(true);
|
||||||
|
chrome.append(&status);
|
||||||
|
root.append(&chrome);
|
||||||
|
|
||||||
|
let flow = FlowBox::new();
|
||||||
|
flow.set_selection_mode(SelectionMode::None);
|
||||||
|
flow.set_homogeneous(true);
|
||||||
|
flow.set_max_children_per_line(6);
|
||||||
|
flow.set_min_children_per_line(2);
|
||||||
|
flow.set_row_spacing(12);
|
||||||
|
flow.set_column_spacing(12);
|
||||||
|
flow.set_halign(Align::Fill);
|
||||||
|
flow.add_css_class("library-grid");
|
||||||
|
|
||||||
|
let scrolled = ScrolledWindow::builder()
|
||||||
|
.hscrollbar_policy(PolicyType::Never)
|
||||||
|
.vscrollbar_policy(PolicyType::Automatic)
|
||||||
|
.vexpand(true)
|
||||||
|
.hexpand(true)
|
||||||
|
.child(&flow)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let empty = Label::new(None);
|
||||||
|
empty.set_wrap(true);
|
||||||
|
empty.set_justify(gtk4::Justification::Center);
|
||||||
|
empty.add_css_class("dim");
|
||||||
|
empty.add_css_class("library-empty");
|
||||||
|
empty.set_hexpand(true);
|
||||||
|
empty.set_vexpand(true);
|
||||||
|
|
||||||
|
let stack = Stack::new();
|
||||||
|
stack.set_vexpand(true);
|
||||||
|
stack.add_named(&scrolled, Some("grid"));
|
||||||
|
stack.add_named(&empty, Some("empty"));
|
||||||
|
root.append(&stack);
|
||||||
|
|
||||||
|
window.set_child(Some(&root));
|
||||||
|
|
||||||
|
let dirs = Rc::new(dirs);
|
||||||
|
let reload = {
|
||||||
|
let dirs = dirs.clone();
|
||||||
|
let flow = flow.clone();
|
||||||
|
let summary = summary.clone();
|
||||||
|
let status = status.clone();
|
||||||
|
let stack = stack.clone();
|
||||||
|
let empty = empty.clone();
|
||||||
|
Rc::new(move || {
|
||||||
|
let papers = library::scan(&dirs);
|
||||||
|
summary.set_text(&dirs_summary(&dirs, papers.len()));
|
||||||
|
empty.set_text(&empty_message(&dirs));
|
||||||
|
if papers.is_empty() {
|
||||||
|
stack.set_visible_child_name("empty");
|
||||||
|
} else {
|
||||||
|
stack.set_visible_child_name("grid");
|
||||||
|
}
|
||||||
|
fill_grid(&flow, &papers, &status);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
reload();
|
||||||
|
{
|
||||||
|
let reload = reload.clone();
|
||||||
|
refresh.connect_clicked(move |_| reload());
|
||||||
|
}
|
||||||
|
|
||||||
|
window.present();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fill_grid(flow: &FlowBox, papers: &[Wallpaper], status: &Label) {
|
||||||
|
while let Some(child) = flow.first_child() {
|
||||||
|
flow.remove(&child);
|
||||||
|
}
|
||||||
|
let current = crate::get().ok();
|
||||||
|
for paper in papers {
|
||||||
|
let is_current = current.as_deref() == Some(paper.path.as_path());
|
||||||
|
flow.insert(&tile(paper, is_current, flow, status), -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tile(paper: &Wallpaper, is_current: bool, flow: &FlowBox, status: &Label) -> Button {
|
||||||
|
let btn = Button::new();
|
||||||
|
btn.add_css_class("wallpaper-tile");
|
||||||
|
btn.set_widget_name(&paper.path.to_string_lossy());
|
||||||
|
btn.set_tooltip_text(Some(&paper.path.to_string_lossy()));
|
||||||
|
if is_current {
|
||||||
|
btn.add_css_class("current");
|
||||||
|
}
|
||||||
|
|
||||||
|
let col = GBox::new(Orientation::Vertical, 0);
|
||||||
|
col.append(&thumbnail(&paper.path));
|
||||||
|
|
||||||
|
let name = Label::new(Some(&paper.name));
|
||||||
|
name.add_css_class("wallpaper-name");
|
||||||
|
name.set_xalign(0.0);
|
||||||
|
name.set_ellipsize(gtk4::pango::EllipsizeMode::End);
|
||||||
|
name.set_max_width_chars(24);
|
||||||
|
col.append(&name);
|
||||||
|
btn.set_child(Some(&col));
|
||||||
|
|
||||||
|
let path = paper.path.clone();
|
||||||
|
let pretty = paper.name.clone();
|
||||||
|
let status = status.clone();
|
||||||
|
let flow = flow.clone();
|
||||||
|
btn.connect_clicked(move |clicked| {
|
||||||
|
if !clicked.is_sensitive() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clicked.set_sensitive(false);
|
||||||
|
status.set_text(&format!("Applying {pretty}…"));
|
||||||
|
let path = path.clone();
|
||||||
|
let pretty = pretty.clone();
|
||||||
|
let status = status.clone();
|
||||||
|
let flow = flow.clone();
|
||||||
|
let clicked = clicked.clone();
|
||||||
|
gtk4::glib::spawn_future_local(async move {
|
||||||
|
let path_thread = path.clone();
|
||||||
|
let result = gtk4::gio::spawn_blocking(move || crate::set(&path_thread)).await;
|
||||||
|
clicked.set_sensitive(true);
|
||||||
|
match result {
|
||||||
|
Ok(Ok(())) => {
|
||||||
|
status.set_text(&format!("Applied {pretty}"));
|
||||||
|
mark_current(&flow, &path);
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => status.set_text(&format!("{e:#}")),
|
||||||
|
Err(_) => status.set_text("Failed to apply wallpaper"),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
btn
|
||||||
|
}
|
||||||
|
|
||||||
|
fn thumbnail(path: &Path) -> Picture {
|
||||||
|
let picture = match Pixbuf::from_file_at_scale(path, THUMB_W, THUMB_H, true) {
|
||||||
|
Ok(pb) => Picture::for_paintable(>k4::gdk::Texture::for_pixbuf(&pb)),
|
||||||
|
Err(_) => Picture::for_filename(path),
|
||||||
|
};
|
||||||
|
picture.set_content_fit(ContentFit::Cover);
|
||||||
|
picture.set_size_request(THUMB_W, THUMB_H);
|
||||||
|
picture.set_can_shrink(true);
|
||||||
|
picture.set_hexpand(true);
|
||||||
|
picture
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark_current(flow: &FlowBox, current: &Path) {
|
||||||
|
let current = current.to_string_lossy();
|
||||||
|
let mut i = 0;
|
||||||
|
while let Some(wrapper) = flow.child_at_index(i) {
|
||||||
|
if let Some(btn) = wrapper
|
||||||
|
.downcast_ref::<FlowBoxChild>()
|
||||||
|
.and_then(|c| c.child())
|
||||||
|
.and_then(|w| w.downcast::<Button>().ok())
|
||||||
|
{
|
||||||
|
if btn.widget_name() == current.as_ref() {
|
||||||
|
btn.add_css_class("current");
|
||||||
|
} else {
|
||||||
|
btn.remove_css_class("current");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dirs_summary(dirs: &[PathBuf], count: usize) -> String {
|
||||||
|
let listed = dirs
|
||||||
|
.iter()
|
||||||
|
.map(|d| {
|
||||||
|
if d.is_dir() {
|
||||||
|
d.display().to_string()
|
||||||
|
} else {
|
||||||
|
format!("{} (missing)", d.display())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" · ");
|
||||||
|
format!("{count} wallpaper(s) · {listed}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn empty_message(dirs: &[PathBuf]) -> String {
|
||||||
|
let listed = dirs
|
||||||
|
.iter()
|
||||||
|
.map(|d| d.display().to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n");
|
||||||
|
format!(
|
||||||
|
"No wallpapers found.\nAdd png/jpg/webp/gif/bmp files under:\n{listed}\n\nOr set library_dirs in {}",
|
||||||
|
crate::config::Config::path().display()
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue