random wallpaper rotator with tags and favorites

init commit

seiso.moe 0588c30b

+1
.gitignore
···
···
+
/target
+773
Cargo.lock
···
···
+
# This file is automatically @generated by Cargo.
+
# It is not intended for manual editing.
+
version = 4
+
+
[[package]]
+
name = "anstream"
+
version = "0.6.18"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
+
dependencies = [
+
"anstyle",
+
"anstyle-parse",
+
"anstyle-query",
+
"anstyle-wincon",
+
"colorchoice",
+
"is_terminal_polyfill",
+
"utf8parse",
+
]
+
+
[[package]]
+
name = "anstyle"
+
version = "1.0.10"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
+
+
[[package]]
+
name = "anstyle-parse"
+
version = "0.2.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
+
dependencies = [
+
"utf8parse",
+
]
+
+
[[package]]
+
name = "anstyle-query"
+
version = "1.1.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
+
dependencies = [
+
"windows-sys",
+
]
+
+
[[package]]
+
name = "anstyle-wincon"
+
version = "3.0.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
+
dependencies = [
+
"anstyle",
+
"once_cell",
+
"windows-sys",
+
]
+
+
[[package]]
+
name = "anyhow"
+
version = "1.0.98"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
+
+
[[package]]
+
name = "bitflags"
+
version = "2.9.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
+
+
[[package]]
+
name = "block-buffer"
+
version = "0.10.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+
dependencies = [
+
"generic-array",
+
]
+
+
[[package]]
+
name = "cc"
+
version = "1.2.21"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8691782945451c1c383942c4874dbe63814f61cb57ef773cda2972682b7bb3c0"
+
dependencies = [
+
"shlex",
+
]
+
+
[[package]]
+
name = "cfg-if"
+
version = "1.0.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+
[[package]]
+
name = "clap"
+
version = "4.5.37"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
+
dependencies = [
+
"clap_builder",
+
"clap_derive",
+
]
+
+
[[package]]
+
name = "clap_builder"
+
version = "4.5.37"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
+
dependencies = [
+
"anstream",
+
"anstyle",
+
"clap_lex",
+
"strsim",
+
]
+
+
[[package]]
+
name = "clap_derive"
+
version = "4.5.32"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
+
dependencies = [
+
"heck",
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "clap_lex"
+
version = "0.7.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
+
+
[[package]]
+
name = "colorchoice"
+
version = "1.0.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
+
+
[[package]]
+
name = "cpufeatures"
+
version = "0.2.17"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+
dependencies = [
+
"libc",
+
]
+
+
[[package]]
+
name = "crypto-common"
+
version = "0.1.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+
dependencies = [
+
"generic-array",
+
"typenum",
+
]
+
+
[[package]]
+
name = "digest"
+
version = "0.10.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+
dependencies = [
+
"block-buffer",
+
"crypto-common",
+
]
+
+
[[package]]
+
name = "dirs"
+
version = "6.0.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+
dependencies = [
+
"dirs-sys",
+
]
+
+
[[package]]
+
name = "dirs-sys"
+
version = "0.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+
dependencies = [
+
"libc",
+
"option-ext",
+
"redox_users",
+
"windows-sys",
+
]
+
+
[[package]]
+
name = "equivalent"
+
version = "1.0.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+
[[package]]
+
name = "erm"
+
version = "0.1.0"
+
dependencies = [
+
"anyhow",
+
"clap",
+
"dirs",
+
"glob",
+
"hex",
+
"rand",
+
"rusqlite",
+
"serde",
+
"sha2",
+
"shellexpand",
+
"toml",
+
"walkdir",
+
]
+
+
[[package]]
+
name = "fallible-iterator"
+
version = "0.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
+
+
[[package]]
+
name = "fallible-streaming-iterator"
+
version = "0.1.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
+
+
[[package]]
+
name = "foldhash"
+
version = "0.1.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+
[[package]]
+
name = "generic-array"
+
version = "0.14.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+
dependencies = [
+
"typenum",
+
"version_check",
+
]
+
+
[[package]]
+
name = "getrandom"
+
version = "0.2.16"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+
dependencies = [
+
"cfg-if",
+
"libc",
+
"wasi 0.11.0+wasi-snapshot-preview1",
+
]
+
+
[[package]]
+
name = "getrandom"
+
version = "0.3.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
+
dependencies = [
+
"cfg-if",
+
"libc",
+
"r-efi",
+
"wasi 0.14.2+wasi-0.2.4",
+
]
+
+
[[package]]
+
name = "glob"
+
version = "0.3.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
+
+
[[package]]
+
name = "hashbrown"
+
version = "0.15.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
+
dependencies = [
+
"foldhash",
+
]
+
+
[[package]]
+
name = "hashlink"
+
version = "0.10.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
+
dependencies = [
+
"hashbrown",
+
]
+
+
[[package]]
+
name = "heck"
+
version = "0.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+
[[package]]
+
name = "hex"
+
version = "0.4.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+
[[package]]
+
name = "indexmap"
+
version = "2.9.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
+
dependencies = [
+
"equivalent",
+
"hashbrown",
+
]
+
+
[[package]]
+
name = "is_terminal_polyfill"
+
version = "1.70.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
+
[[package]]
+
name = "libc"
+
version = "0.2.172"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
+
+
[[package]]
+
name = "libredox"
+
version = "0.1.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
+
dependencies = [
+
"bitflags",
+
"libc",
+
]
+
+
[[package]]
+
name = "libsqlite3-sys"
+
version = "0.33.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "947e6816f7825b2b45027c2c32e7085da9934defa535de4a6a46b10a4d5257fa"
+
dependencies = [
+
"cc",
+
"pkg-config",
+
"vcpkg",
+
]
+
+
[[package]]
+
name = "memchr"
+
version = "2.7.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+
[[package]]
+
name = "once_cell"
+
version = "1.21.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+
[[package]]
+
name = "option-ext"
+
version = "0.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+
[[package]]
+
name = "pkg-config"
+
version = "0.3.32"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+
[[package]]
+
name = "ppv-lite86"
+
version = "0.2.21"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+
dependencies = [
+
"zerocopy",
+
]
+
+
[[package]]
+
name = "proc-macro2"
+
version = "1.0.95"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+
dependencies = [
+
"unicode-ident",
+
]
+
+
[[package]]
+
name = "quote"
+
version = "1.0.40"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+
dependencies = [
+
"proc-macro2",
+
]
+
+
[[package]]
+
name = "r-efi"
+
version = "5.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
+
+
[[package]]
+
name = "rand"
+
version = "0.9.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
+
dependencies = [
+
"rand_chacha",
+
"rand_core",
+
]
+
+
[[package]]
+
name = "rand_chacha"
+
version = "0.9.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+
dependencies = [
+
"ppv-lite86",
+
"rand_core",
+
]
+
+
[[package]]
+
name = "rand_core"
+
version = "0.9.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
+
dependencies = [
+
"getrandom 0.3.2",
+
]
+
+
[[package]]
+
name = "redox_users"
+
version = "0.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
+
dependencies = [
+
"getrandom 0.2.16",
+
"libredox",
+
"thiserror",
+
]
+
+
[[package]]
+
name = "rusqlite"
+
version = "0.35.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a22715a5d6deef63c637207afbe68d0c72c3f8d0022d7cf9714c442d6157606b"
+
dependencies = [
+
"bitflags",
+
"fallible-iterator",
+
"fallible-streaming-iterator",
+
"hashlink",
+
"libsqlite3-sys",
+
"smallvec",
+
]
+
+
[[package]]
+
name = "same-file"
+
version = "1.0.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+
dependencies = [
+
"winapi-util",
+
]
+
+
[[package]]
+
name = "serde"
+
version = "1.0.219"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+
dependencies = [
+
"serde_derive",
+
]
+
+
[[package]]
+
name = "serde_derive"
+
version = "1.0.219"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "serde_spanned"
+
version = "0.6.8"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
+
dependencies = [
+
"serde",
+
]
+
+
[[package]]
+
name = "sha2"
+
version = "0.10.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+
dependencies = [
+
"cfg-if",
+
"cpufeatures",
+
"digest",
+
]
+
+
[[package]]
+
name = "shellexpand"
+
version = "3.1.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb"
+
dependencies = [
+
"dirs",
+
]
+
+
[[package]]
+
name = "shlex"
+
version = "1.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+
[[package]]
+
name = "smallvec"
+
version = "1.15.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
+
+
[[package]]
+
name = "strsim"
+
version = "0.11.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+
[[package]]
+
name = "syn"
+
version = "2.0.101"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"unicode-ident",
+
]
+
+
[[package]]
+
name = "thiserror"
+
version = "2.0.12"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
+
dependencies = [
+
"thiserror-impl",
+
]
+
+
[[package]]
+
name = "thiserror-impl"
+
version = "2.0.12"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "toml"
+
version = "0.8.22"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae"
+
dependencies = [
+
"serde",
+
"serde_spanned",
+
"toml_datetime",
+
"toml_edit",
+
]
+
+
[[package]]
+
name = "toml_datetime"
+
version = "0.6.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
+
dependencies = [
+
"serde",
+
]
+
+
[[package]]
+
name = "toml_edit"
+
version = "0.22.26"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
+
dependencies = [
+
"indexmap",
+
"serde",
+
"serde_spanned",
+
"toml_datetime",
+
"toml_write",
+
"winnow",
+
]
+
+
[[package]]
+
name = "toml_write"
+
version = "0.1.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076"
+
+
[[package]]
+
name = "typenum"
+
version = "1.18.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
+
+
[[package]]
+
name = "unicode-ident"
+
version = "1.0.18"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+
[[package]]
+
name = "utf8parse"
+
version = "0.2.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+
[[package]]
+
name = "vcpkg"
+
version = "0.2.15"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+
[[package]]
+
name = "version_check"
+
version = "0.9.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+
[[package]]
+
name = "walkdir"
+
version = "2.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+
dependencies = [
+
"same-file",
+
"winapi-util",
+
]
+
+
[[package]]
+
name = "wasi"
+
version = "0.11.0+wasi-snapshot-preview1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+
[[package]]
+
name = "wasi"
+
version = "0.14.2+wasi-0.2.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+
dependencies = [
+
"wit-bindgen-rt",
+
]
+
+
[[package]]
+
name = "winapi-util"
+
version = "0.1.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
+
dependencies = [
+
"windows-sys",
+
]
+
+
[[package]]
+
name = "windows-sys"
+
version = "0.59.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+
dependencies = [
+
"windows-targets",
+
]
+
+
[[package]]
+
name = "windows-targets"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+
dependencies = [
+
"windows_aarch64_gnullvm",
+
"windows_aarch64_msvc",
+
"windows_i686_gnu",
+
"windows_i686_gnullvm",
+
"windows_i686_msvc",
+
"windows_x86_64_gnu",
+
"windows_x86_64_gnullvm",
+
"windows_x86_64_msvc",
+
]
+
+
[[package]]
+
name = "windows_aarch64_gnullvm"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+
[[package]]
+
name = "windows_aarch64_msvc"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+
[[package]]
+
name = "windows_i686_gnu"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+
[[package]]
+
name = "windows_i686_gnullvm"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+
[[package]]
+
name = "windows_i686_msvc"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+
[[package]]
+
name = "windows_x86_64_gnu"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+
[[package]]
+
name = "windows_x86_64_gnullvm"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+
[[package]]
+
name = "windows_x86_64_msvc"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+
[[package]]
+
name = "winnow"
+
version = "0.7.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d9fb597c990f03753e08d3c29efbfcf2019a003b4bf4ba19225c158e1549f0f3"
+
dependencies = [
+
"memchr",
+
]
+
+
[[package]]
+
name = "wit-bindgen-rt"
+
version = "0.39.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
+
dependencies = [
+
"bitflags",
+
]
+
+
[[package]]
+
name = "zerocopy"
+
version = "0.8.25"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
+
dependencies = [
+
"zerocopy-derive",
+
]
+
+
[[package]]
+
name = "zerocopy-derive"
+
version = "0.8.25"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+18
Cargo.toml
···
···
+
[package]
+
name = "erm"
+
version = "0.1.0"
+
edition = "2024"
+
+
[dependencies]
+
anyhow = "1.0.98"
+
clap = { version = "4.5.37", features = ["derive", "cargo"] }
+
dirs = "6.0.0"
+
glob = "0.3.2"
+
rand = "0.9.1"
+
rusqlite = { version = "0.35.0", features = ["bundled"] }
+
serde = { version = "1.0.219", features = ["derive"] }
+
shellexpand = "3.1.1"
+
toml = "0.8.22"
+
walkdir = "2.5.0"
+
sha2 = "0.10.9"
+
hex = "0.4.3"
+3
README.md
···
···
+
# erm :3
+
rotating image wallpaper setter with tags and favorites
+
+85
src/config.rs
···
···
+
use dirs::config_dir;
+
use anyhow::{Context, Result};
+
use serde::Deserialize;
+
use std::fs::{self};
+
use std::path::{Path, PathBuf};
+
+
#[derive(Deserialize, Debug)]
+
pub struct Config {
+
// list of directories to be searched
+
pub directories: Vec<String>,
+
// recursion?
+
pub include_subdirectories: Option<bool>,
+
// any file extensions to be checked
+
pub file_extensions: Option<Vec<String>>,
+
// exclude anything
+
pub exclude_patterns: Option<Vec<String>>,
+
// extra swaybg args
+
pub swaybg_args: Option<Vec<String>>,
+
}
+
+
pub fn get_config_dir() -> Result<PathBuf> {
+
let config_dir = config_dir()
+
.context("Failed to determine XDG config directory")?
+
.join("erm");
+
+
if !config_dir.exists() {
+
fs::create_dir_all(&config_dir).with_context(|| {
+
format!(
+
"Failed to create config directory: {}",
+
config_dir.display()
+
)
+
})?;
+
}
+
+
Ok(config_dir)
+
}
+
+
pub fn create_default_config(path: &Path) -> Result<()> {
+
let default_config = r#"# erm configuration
+
+
# Directories to search for background images (use absolute paths or ~)
+
directories = [
+
"~/Pictures/Wallpapers",
+
"~/Pictures/Backgrounds"
+
]
+
+
# Whether to include subdirectories (default: true)
+
include_subdirectories = true
+
+
# File extensions to include (case-insensitive, default: ["jpg", "jpeg", "png", "webp"])
+
file_extensions = ["jpg", "jpeg", "png", "webp"]
+
+
# Patterns to exclude (glob patterns applied to the full path)
+
exclude_patterns = ["*thumbnail*", "*small*"]
+
+
# Additional arguments for swaybg (e.g., ["--output", "HDMI-A-1", "--mode", "fill"])
+
swaybg_args = ["--mode", "fill"]
+
"#;
+
+
fs::write(path, default_config)
+
.with_context(|| format!("Failed to write default config to: {}", path.display()))?;
+
println!("Created default configuration at: {}", path.display());
+
+
Ok(())
+
}
+
+
pub fn load_config(path: &Path) -> Result<Config> {
+
let content = fs::read_to_string(path)
+
.with_context(|| format!("Failed to read config at: {}", path.display()))?;
+
+
let config: Config = toml::from_str(&content).context("Failed to parse TOML configuration")?;
+
+
// only support ~ expansion for now relative paths are @-@
+
for dir in &config.directories {
+
if !dir.starts_with('/') && !dir.starts_with('~') {
+
eprintln!(
+
"Warning: Relative path found in config directories: '{}'. Consider using absolute paths or paths starting with '~'.",
+
dir
+
);
+
}
+
}
+
+
Ok(config)
+
}
+
+689
src/db.rs
···
···
+
// this entire file could probably be replaced with an actual orm or
+
// something but im too lazy to find one thats good enough
+
+
use crate::config::*;
+
use crate::utils::calculate_image_hash;
+
+
use std::collections::HashMap;
+
use anyhow::{Context, Result};
+
use rand::prelude::*;
+
use rusqlite::{Connection, OptionalExtension, Result as SqlResult, params};
+
use std::collections::HashSet;
+
use std::fs::{self};
+
use std::path::{Path, PathBuf};
+
use std::process::{Command, Stdio};
+
use std::time::{SystemTime, UNIX_EPOCH};
+
use walkdir::WalkDir;
+
+
pub fn setup_database(path: &Path) -> Result<Connection> {
+
let conn = Connection::open(path)
+
.with_context(|| format!("Failed to open or create database: {}", path.display()))?;
+
+
conn.execute("PRAGMA foreign_keys = ON;", [])?;
+
+
conn.execute(
+
"CREATE TABLE IF NOT EXISTS images (
+
id TEXT PRIMARY KEY NOT NULL,
+
path TEXT NOT NULL UNIQUE,
+
last_used INTEGER NOT NULL DEFAULT 0,
+
used_count INTEGER NOT NULL DEFAULT 0,
+
favorite BOOLEAN NOT NULL DEFAULT 0 CHECK (favorite IN (0, 1))
+
)",
+
[],
+
)?;
+
+
conn.execute(
+
"CREATE TABLE IF NOT EXISTS tags (
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
+
name TEXT NOT NULL UNIQUE COLLATE NOCASE
+
)",
+
[],
+
)?;
+
+
conn.execute(
+
"CREATE TABLE IF NOT EXISTS image_tags (
+
image_id TEXT NOT NULL,
+
tag_id INTEGER NOT NULL,
+
PRIMARY KEY (image_id, tag_id),
+
FOREIGN KEY (image_id) REFERENCES images (id) ON DELETE CASCADE,
+
FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE
+
)",
+
[],
+
)?;
+
+
Ok(conn)
+
}
+
+
pub fn create_indices(conn: &Connection) -> Result<()> {
+
conn.execute(
+
"CREATE INDEX IF NOT EXISTS idx_images_path ON images(path);",
+
[],
+
)?;
+
conn.execute(
+
"CREATE INDEX IF NOT EXISTS idx_images_favorite ON images(favorite);",
+
[],
+
)?;
+
conn.execute(
+
"CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name);",
+
[],
+
)?;
+
conn.execute(
+
"CREATE INDEX IF NOT EXISTS idx_image_tags_tag_id ON image_tags(tag_id);",
+
[],
+
)?;
+
Ok(())
+
}
+
+
pub fn get_image_id(conn: &Connection, path: &Path) -> SqlResult<Option<String>> {
+
conn.query_row(
+
"SELECT id FROM images WHERE path = ?",
+
params![path.to_string_lossy()],
+
|row| row.get(0),
+
)
+
.optional()
+
}
+
+
pub fn get_or_create_tag_id(conn: &Connection, tag: &str) -> SqlResult<i64> {
+
conn.execute("INSERT OR IGNORE INTO tags (name) VALUES (?)", params![tag])?;
+
+
conn.query_row(
+
"SELECT id FROM tags WHERE name = ? COLLATE NOCASE",
+
params![tag],
+
|row| row.get(0),
+
)
+
}
+
+
pub fn get_or_insert_image_id(conn: &Connection, path: &Path) -> Result<String> {
+
let path_str = path.to_string_lossy();
+
+
if let Some(id) = get_image_id(conn, path)? {
+
Ok(id)
+
} else {
+
println!(
+
"Image not found in DB, calculating hash and adding: {}",
+
path.display()
+
);
+
let hash_id = calculate_image_hash(path).with_context(|| {
+
format!("Failed to calculate hash for new image: {}", path.display())
+
})?;
+
+
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
+
+
conn.execute(
+
"INSERT INTO images (id, path, last_used, used_count, favorite) VALUES (?, ?, ?, 0, 0)",
+
params![hash_id, path_str, now],
+
)
+
.with_context(|| {
+
format!(
+
"Failed to insert new image into database: {}",
+
path.display()
+
)
+
})?;
+
+
Ok(hash_id)
+
}
+
}
+
+
pub fn add_tag(conn: &Connection, path: &Path, tag: &str) -> Result<()> {
+
let image_id = get_or_insert_image_id(conn, path)?;
+
let tag_id = get_or_create_tag_id(conn, tag)?;
+
+
let changed = conn.execute(
+
"INSERT OR IGNORE INTO image_tags (image_id, tag_id) VALUES (?, ?)",
+
params![image_id, tag_id],
+
)?;
+
+
if changed > 0 {
+
println!("Added tag '{}' to image: {}", tag, path.display());
+
} else {
+
println!(
+
"Tag '{}' already present for image: {}",
+
tag,
+
path.display()
+
);
+
}
+
+
Ok(())
+
}
+
+
pub fn remove_tag(conn: &Connection, path: &Path, tag: &str) -> Result<()> {
+
let image_id_opt = get_image_id(conn, path)?;
+
+
if let Some(image_id) = image_id_opt {
+
let tag_id_opt: SqlResult<Option<i64>> = conn
+
.query_row(
+
"SELECT id FROM tags WHERE name = ? COLLATE NOCASE",
+
params![tag],
+
|row| row.get(0),
+
)
+
.optional();
+
+
match tag_id_opt {
+
Ok(Some(tag_id)) => {
+
let changed = conn.execute(
+
"DELETE FROM image_tags WHERE image_id = ? AND tag_id = ?",
+
params![image_id, tag_id],
+
)?;
+
+
if changed > 0 {
+
println!("Removed tag '{}' from image: {}", tag, path.display());
+
} else {
+
println!("Tag '{}' was not found on image: {}", tag, path.display());
+
}
+
}
+
Ok(None) => {
+
println!("Tag '{}' not found in database.", tag);
+
}
+
Err(e) => {
+
return Err(e).context("Database error checking for tag ID");
+
}
+
}
+
} else {
+
println!("Image not found in database: {}", path.display());
+
}
+
+
Ok(())
+
}
+
+
pub fn list_tags(conn: &Connection, path: &Path) -> Result<()> {
+
let image_id_opt = get_image_id(conn, path)?;
+
+
if let Some(image_id) = image_id_opt {
+
let mut stmt = conn.prepare(
+
"SELECT t.name FROM tags t
+
JOIN image_tags it ON t.id = it.tag_id
+
WHERE it.image_id = ?
+
ORDER BY t.name COLLATE NOCASE",
+
)?;
+
let tags: Vec<String> = stmt
+
.query_map(params![image_id], |row| row.get(0))?
+
.collect::<SqlResult<_>>()?;
+
+
let favorite: bool = conn.query_row(
+
"SELECT favorite FROM images WHERE id = ?",
+
params![image_id],
+
|row| row.get(0),
+
)?;
+
+
println!("Details for image: {}", path.display());
+
println!(
+
" Status: {}",
+
if favorite {
+
"Favorite ★"
+
} else {
+
"Not Favorite"
+
}
+
);
+
+
if tags.is_empty() {
+
println!(" Tags: None");
+
} else {
+
println!(" Tags:");
+
for tag in tags {
+
println!(" - {}", tag);
+
}
+
}
+
} else {
+
println!("Image not found in database: {}", path.display());
+
}
+
+
Ok(())
+
}
+
+
pub fn add_favorite(conn: &Connection, path: &Path) -> Result<()> {
+
let image_id = get_or_insert_image_id(conn, path)?;
+
+
let changed = conn.execute(
+
"UPDATE images SET favorite = 1 WHERE id = ?",
+
params![image_id],
+
)?;
+
+
if changed > 0 {
+
println!("Marked image as favorite: {}", path.display());
+
} else {
+
println!(
+
"Image already marked as favorite or not found for update: {}",
+
path.display()
+
);
+
}
+
+
Ok(())
+
}
+
+
pub fn remove_favorite(conn: &Connection, path: &Path) -> Result<()> {
+
let image_id_opt = get_image_id(conn, path)?;
+
+
if let Some(image_id) = image_id_opt {
+
let changed = conn.execute(
+
"UPDATE images SET favorite = 0 WHERE id = ?",
+
params![image_id],
+
)?;
+
+
if changed > 0 {
+
println!("Removed image from favorites: {}", path.display());
+
} else {
+
println!(
+
"Image was not marked as favorite or not found for update: {}",
+
path.display()
+
);
+
}
+
} else {
+
println!("Image not found in database: {}", path.display());
+
}
+
+
Ok(())
+
}
+
+
pub fn list_images(conn: &Connection, tag: Option<&str>, favorite_only: bool) -> Result<()> {
+
let mut query_parts = vec!["SELECT i.path, i.used_count, i.favorite FROM images i"];
+
let mut conditions = vec![];
+
+
if let Some(_t) = tag {
+
query_parts.push("JOIN image_tags it ON i.id = it.image_id");
+
query_parts.push("JOIN tags t ON it.tag_id = t.id");
+
conditions.push("t.name = ? COLLATE NOCASE");
+
}
+
+
if favorite_only {
+
conditions.push("i.favorite = 1");
+
}
+
+
let conditions_str;
+
if !conditions.is_empty() {
+
query_parts.push("WHERE");
+
conditions_str = conditions.join(" AND ");
+
query_parts.push(&conditions_str);
+
}
+
+
query_parts.push("ORDER BY i.path COLLATE NOCASE");
+
+
let final_query = query_parts.join(" ");
+
let mut stmt = conn.prepare(&final_query)?;
+
+
let row_mapper = |row: &rusqlite::Row| {
+
Ok((
+
row.get::<_, String>(0)?,
+
row.get::<_, u32>(1)?,
+
row.get::<_, bool>(2)?,
+
))
+
};
+
+
let images_result = if let Some(t) = tag {
+
stmt.query_map(params![t], row_mapper)
+
} else {
+
stmt.query_map([], row_mapper)
+
};
+
+
let images: Vec<(String, u32, bool)> = images_result?
+
.collect::<SqlResult<Vec<_>>>()
+
.context("Failed to retrieve image list from database")?;
+
+
if images.is_empty() {
+
let filter_desc = match (tag, favorite_only) {
+
(Some(t), true) => format!("favorite images with tag '{}'", t),
+
(Some(t), false) => format!("images with tag '{}'", t),
+
(None, true) => "favorite images".to_string(),
+
(None, false) => "all images".to_string(),
+
};
+
println!("No {} found in the database.", filter_desc);
+
} else {
+
let filter_desc = match (tag, favorite_only) {
+
(Some(t), true) => format!("Favorite images with tag '{}'", t),
+
(Some(t), false) => format!("Images with tag '{}'", t),
+
(None, true) => "Favorite images".to_string(),
+
(None, false) => "All images".to_string(),
+
};
+
+
println!("Listing {}:", filter_desc);
+
println!("{:<2} {:<6} {}", "★", "Uses", "Path");
+
println!("{:-<2} {:-<6} {:-<10}", "", "", "");
+
for (path, uses, favorite) in &images {
+
let star = if *favorite { "★" } else { " " };
+
println!("{:<2} {:<6} {}", star, uses, path);
+
}
+
println!("\nTotal: {} images listed.", images.len());
+
}
+
+
Ok(())
+
}
+
+
pub fn get_current_wallpaper(conn: &Connection) -> Result<String> {
+
conn.execute(
+
"CREATE TABLE IF NOT EXISTS settings (
+
key TEXT PRIMARY KEY NOT NULL,
+
value TEXT NOT NULL
+
)",
+
[],
+
)?;
+
+
match conn.query_row(
+
"SELECT value FROM settings WHERE key = 'current_wallpaper'",
+
[],
+
|row| row.get::<_, String>(0),
+
) {
+
Ok(path) => {
+
let path_buf = PathBuf::from(&path);
+
if path_buf.exists() {
+
Ok(path)
+
} else {
+
anyhow::bail!("Current wallpaper file no longer exists: {}", path)
+
}
+
}
+
Err(rusqlite::Error::QueryReturnedNoRows) => {
+
anyhow::bail!(
+
"Current wallpaper not set. Please use 'erm random' first or specify a path."
+
)
+
}
+
Err(e) => {
+
anyhow::bail!("Error retrieving current wallpaper: {}", e)
+
}
+
}
+
}
+
+
+
pub fn refresh_database(conn: &mut Connection, config: &Config) -> Result<()> {
+
println!("Starting database refresh...");
+
let start_time = std::time::Instant::now();
+
+
let tx = conn.transaction()?;
+
+
let mut db_images_stmt = tx.prepare("SELECT id, path FROM images")?;
+
let db_images: HashMap<String, String> = db_images_stmt
+
.query_map([], |row| Ok((row.get(1)?, row.get(0)?)))?
+
.collect::<SqlResult<HashMap<String, String>>>()?;
+
drop(db_images_stmt);
+
+
let mut current_fs_paths: HashSet<PathBuf> = HashSet::new();
+
let mut discovered_count = 0;
+
+
let extensions = config
+
.file_extensions
+
.as_ref()
+
.map(|ext| {
+
ext.iter()
+
.map(|s| format!(".{}", s.to_lowercase()))
+
.collect::<HashSet<_>>()
+
})
+
.unwrap_or_else(|| {
+
[".jpg", ".jpeg", ".png", ".webp"]
+
.iter()
+
.map(|&s| s.to_string())
+
.collect()
+
});
+
+
let exclude_patterns = config
+
.exclude_patterns
+
.as_ref()
+
.map(|patterns| {
+
patterns
+
.iter()
+
.filter_map(|p| glob::Pattern::new(p).ok())
+
.collect::<Vec<_>>()
+
})
+
.unwrap_or_default();
+
+
for dir_path_str in &config.directories {
+
let expanded_path_str = shellexpand::tilde(dir_path_str).into_owned();
+
let start_path = Path::new(&expanded_path_str);
+
+
if !start_path.is_dir() {
+
eprintln!(
+
"Warning: Configured directory does not exist or is not a directory: {}",
+
start_path.display()
+
);
+
continue;
+
}
+
+
println!("Scanning directory: {}", start_path.display());
+
+
let include_subdirs = config.include_subdirectories.unwrap_or(true);
+
let mut walker = WalkDir::new(start_path).follow_links(true);
+
if !include_subdirs {
+
walker = walker.max_depth(1);
+
}
+
+
for entry_result in walker.into_iter() {
+
let entry = match entry_result {
+
Ok(e) => e,
+
Err(e) => {
+
eprintln!("Warning: Error traversing directory: {}", e);
+
continue;
+
}
+
};
+
+
if !entry.file_type().is_file() {
+
continue;
+
}
+
+
let file_path = entry.path();
+
+
if let Some(ext) = file_path.extension().and_then(|e| e.to_str()) {
+
if !extensions.contains(&format!(".{}", ext.to_lowercase())) {
+
continue;
+
}
+
} else {
+
continue;
+
}
+
+
let canonical_path = match fs::canonicalize(file_path) {
+
Ok(p) => p,
+
Err(e) => {
+
eprintln!(
+
"Warning: Could not canonicalize path {}: {}",
+
file_path.display(),
+
e
+
);
+
continue;
+
}
+
};
+
let canonical_path_str = canonical_path.to_string_lossy();
+
+
let should_exclude = exclude_patterns
+
.iter()
+
.any(|pattern| pattern.matches(&canonical_path_str));
+
+
if should_exclude {
+
continue;
+
}
+
+
discovered_count += 1;
+
current_fs_paths.insert(canonical_path);
+
}
+
}
+
+
println!(
+
"Discovered {} potential image files on disk.",
+
discovered_count
+
);
+
+
let db_paths: HashSet<String> = db_images.keys().cloned().collect();
+
let fs_paths_str: HashSet<String> = current_fs_paths
+
.iter()
+
.map(|p| p.to_string_lossy().into_owned())
+
.collect();
+
+
let paths_to_remove = db_paths
+
.difference(&fs_paths_str)
+
.cloned()
+
.collect::<Vec<_>>();
+
let mut removed_count = 0;
+
if !paths_to_remove.is_empty() {
+
println!(
+
"Removing {} entries from database that no longer exist on disk...",
+
paths_to_remove.len()
+
);
+
let mut delete_stmt = tx.prepare("DELETE FROM images WHERE path = ?")?;
+
for path_str in paths_to_remove {
+
match delete_stmt.execute(params![path_str]) {
+
Ok(count) => removed_count += count,
+
Err(e) => eprintln!("Error deleting image with path {}: {}", path_str, e),
+
}
+
}
+
}
+
+
let paths_to_add = fs_paths_str.difference(&db_paths).collect::<Vec<_>>();
+
let mut added_count = 0;
+
let mut hash_errors = 0;
+
if !paths_to_add.is_empty() {
+
println!(
+
"Adding {} new images to the database...",
+
paths_to_add.len()
+
);
+
let mut insert_stmt = tx.prepare(
+
"INSERT OR IGNORE INTO images (id, path, last_used, used_count, favorite) VALUES (?, ?, 0, 0, 0)",
+
)?;
+
for path_str in paths_to_add {
+
print!("added {} to database\r", added_count);
+
let path = PathBuf::from(path_str);
+
match calculate_image_hash(&path) {
+
Ok(hash_id) => {
+
match insert_stmt.execute(params![hash_id, path_str]) {
+
Ok(1) => added_count += 1,
+
Ok(0) => { /* id or path already existed*/ }
+
Ok(_) => { unreachable!() }
+
Err(e) => eprintln!("error inserting image with path {}: {}", path_str, e),
+
}
+
}
+
Err(e) => {
+
eprintln!(
+
"error calculating hash for {}: {}. Skipping file.",
+
path_str, e
+
);
+
hash_errors += 1;
+
}
+
}
+
}
+
println!();
+
}
+
+
tx.commit()?;
+
+
let duration = start_time.elapsed();
+
println!(
+
"database refresh completed in {:.2?}. Added: {}, Removed: {}{}",
+
duration,
+
added_count,
+
removed_count,
+
if hash_errors > 0 {
+
format!(", hash errors: {}", hash_errors)
+
} else {
+
"".to_string()
+
}
+
);
+
+
Ok(())
+
}
+
+
pub fn select_wallpaper(
+
conn: &Connection,
+
tag: Option<&str>,
+
favorite_only: bool,
+
) -> Result<Option<String>> {
+
let mut query_parts = vec!["SELECT i.path FROM images i"];
+
let mut conditions = vec![];
+
+
if let Some(_t) = tag {
+
query_parts.push("JOIN image_tags it ON i.id = it.image_id");
+
query_parts.push("JOIN tags t ON it.tag_id = t.id");
+
conditions.push("t.name = ? COLLATE NOCASE");
+
}
+
+
if favorite_only {
+
conditions.push("i.favorite = 1");
+
}
+
+
let conditions_str = conditions.join(" AND ");
+
query_parts.push(&conditions_str);
+
+
query_parts.push("ORDER BY i.used_count ASC, RANDOM()");
+
query_parts.push("LIMIT 50");
+
+
let final_query = query_parts.join(" ");
+
let mut stmt = conn.prepare(&final_query)?;
+
+
let candidate_paths: Vec<String> = if let Some(t) = tag {
+
stmt.query_map(params![t], |row| row.get(0))?
+
.collect::<SqlResult<Vec<String>>>()?
+
} else {
+
stmt.query_map([], |row| row.get(0))?
+
.collect::<SqlResult<Vec<String>>>()?
+
};
+
+
if candidate_paths.is_empty() {
+
Ok(None)
+
} else {
+
let mut rng = rand::rng();
+
Ok(candidate_paths.choose(&mut rng).cloned())
+
}
+
}
+
+
pub fn update_usage(conn: &Connection, path: &str) -> Result<()> {
+
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
+
+
let updated_rows = conn.execute(
+
"UPDATE images SET last_used = ?, used_count = used_count + 1 WHERE path = ?",
+
params![now, path],
+
)?;
+
+
if updated_rows == 0 {
+
eprintln!(
+
"Warning: Tried to update usage for path not found in DB: {}",
+
path
+
);
+
}
+
+
Ok(())
+
}
+
+
pub fn set_wallpaper(conn: &Connection, path_str: &str, config: &Config) -> Result<()> {
+
let path = Path::new(path_str);
+
if !path.exists() {
+
anyhow::bail!("Wallpaper file does not exist: {}", path_str);
+
}
+
+
let pgrep_output = Command::new("pgrep").arg("-x").arg("swaybg").output();
+
+
if let Ok(output) = pgrep_output {
+
if output.status.success() {
+
println!("Found existing swaybg process, terminating it...");
+
Command::new("pkill")
+
.arg("-x")
+
.arg("swaybg")
+
.status()
+
.context("Failed to terminate existing swaybg process")?;
+
std::thread::sleep(std::time::Duration::from_millis(100));
+
}
+
}
+
+
let mut cmd = Command::new("swaybg");
+
cmd.arg("-i").arg(path);
+
+
if let Some(args) = &config.swaybg_args {
+
cmd.args(args);
+
}
+
+
println!("Setting wallpaper: {}", path.display());
+
let child = cmd
+
.stdin(Stdio::null())
+
.stdout(Stdio::null())
+
.stderr(Stdio::null())
+
.spawn()
+
.context("Failed to spawn swaybg in daemon mode")?;
+
println!("Started swaybg daemon (PID: {})", child.id());
+
std::mem::forget(child);
+
+
conn.execute(
+
"CREATE TABLE IF NOT EXISTS settings (
+
key TEXT PRIMARY KEY NOT NULL,
+
value TEXT NOT NULL
+
)",
+
[],
+
)?;
+
+
conn.execute(
+
"INSERT OR REPLACE INTO settings (key, value) VALUES ('current_wallpaper', ?)",
+
params![path_str],
+
)?;
+
+
Ok(())
+
}
+209
src/main.rs
···
···
+
mod config;
+
mod db;
+
mod utils;
+
+
use crate::config::*;
+
use crate::db::*;
+
use anyhow::{Context, Result};
+
use clap::{Parser, Subcommand};
+
use rusqlite::params;
+
use std::fs::{self};
+
use std::path::{PathBuf};
+
+
#[derive(Parser, Debug)]
+
#[clap(
+
name = "erm",
+
about = "background rotator using swaybg with tagging support"
+
)]
+
struct Args {
+
#[command(subcommand)]
+
command: Commands,
+
}
+
+
#[derive(Subcommand, Debug)]
+
enum Commands {
+
/// Set a random background
+
Random {
+
/// Filter by tag
+
#[arg(short, long)]
+
tag: Option<String>,
+
+
/// Use only favorites
+
#[arg(short, long)]
+
favorite: bool,
+
+
/// Don't update the database (just set a new background)
+
#[arg(short, long)]
+
quick: bool,
+
},
+
+
/// Refresh the image database
+
Refresh,
+
+
/// Tag management
+
Tag {
+
/// The path to the image
+
path: Option<String>,
+
+
/// Tag to add
+
#[arg(short, long)]
+
add: Option<String>,
+
+
/// Tag to remove
+
#[arg(short, long)]
+
remove: Option<String>,
+
+
/// List all tags for the image
+
#[arg(short, long)]
+
list: bool,
+
},
+
+
/// Favorite management
+
Favorite {
+
/// The path to the image
+
path: Option<String>,
+
+
/// Add to favorites
+
#[arg(short, long)]
+
add: bool,
+
+
/// Remove from favorites
+
#[arg(short, long)]
+
remove: bool,
+
},
+
+
/// List images
+
List {
+
/// Filter by tag
+
#[arg(short, long)]
+
tag: Option<String>,
+
+
/// Show only favorites
+
#[arg(short, long)]
+
favorite: bool,
+
},
+
+
/// Current
+
Current,
+
}
+
+
fn main() -> Result<()> {
+
let args = Args::parse();
+
+
let config_dir = get_config_dir()?;
+
let config_path = config_dir.join("config.toml");
+
let db_path = config_dir.join("images.db");
+
+
if !config_path.exists() {
+
create_default_config(&config_path)?;
+
}
+
+
let config = load_config(&config_path)?;
+
let mut conn = setup_database(&db_path)?;
+
+
create_indices(&conn)?;
+
+
match &args.command {
+
Commands::Random {
+
tag,
+
favorite,
+
quick,
+
} => {
+
let wallpaper = select_wallpaper(&conn, tag.as_deref(), *favorite)?;
+
+
if let Some(wallpaper_path) = wallpaper {
+
if !*quick {
+
update_usage(&conn, &wallpaper_path)?;
+
}
+
set_wallpaper(&conn, &wallpaper_path, &config)?;
+
} else {
+
let filter_desc = match (tag, favorite) {
+
(Some(t), true) => format!("favorite wallpapers with tag '{}'", t),
+
(Some(t), false) => format!("wallpapers with tag '{}'", t),
+
(None, true) => "favorite wallpapers".to_string(),
+
(None, false) => "wallpapers".to_string(),
+
};
+
eprintln!(
+
"No {} found. Try running 'erm refresh' or adjusting filters.",
+
filter_desc
+
);
+
}
+
}
+
Commands::Refresh => {
+
refresh_database(&mut conn, &config)?;
+
}
+
Commands::Current => {
+
let current = get_current_wallpaper(&conn)?;
+
println!("{}", current);
+
}
+
Commands::Tag {
+
path,
+
add,
+
remove,
+
list,
+
} => {
+
let path = match path {
+
Some(p) => resolve_path(p)?,
+
None => {
+
let current = get_current_wallpaper(&conn)?;
+
resolve_path(&current)?
+
}
+
};
+
+
if *list {
+
list_tags(&conn, &path)?;
+
} else if let Some(tag) = add {
+
add_tag(&conn, &path, tag)?;
+
} else if let Some(tag) = remove {
+
remove_tag(&conn, &path, tag)?;
+
} else {
+
list_tags(&conn, &path)?;
+
}
+
}
+
Commands::Favorite { path, add, remove } => {
+
let path = match path {
+
Some(p) => resolve_path(p)?,
+
None => {
+
let current = get_current_wallpaper(&conn)?;
+
resolve_path(&current)?
+
}
+
};
+
+
match (*add, *remove) {
+
(true, false) => add_favorite(&conn, &path)?,
+
(false, true) => remove_favorite(&conn, &path)?,
+
(true, true) => {
+
anyhow::bail!("Cannot add and remove favorite status simultaneously.")
+
}
+
(false, false) => {
+
match get_image_id(&conn, &path)? {
+
Some(id) => {
+
let is_fav: bool = conn.query_row(
+
"SELECT favorite FROM images WHERE id = ?",
+
params![id],
+
|row| row.get(0),
+
)?;
+
println!(
+
"Image '{}' is {}a favorite.",
+
path.display(),
+
if is_fav { "" } else { "not " }
+
);
+
}
+
None => println!("Image not found in database: {}", path.display()),
+
}
+
}
+
}
+
}
+
Commands::List { tag, favorite } => {
+
list_images(&conn, tag.as_deref(), *favorite)?;
+
}
+
}
+
+
Ok(())
+
}
+
+
fn resolve_path(path_str: &str) -> Result<PathBuf> {
+
let expanded = shellexpand::tilde(path_str).into_owned();
+
fs::canonicalize(&expanded)
+
.with_context(|| format!("Failed to find or resolve path: {}", expanded))
+
}
+29
src/utils.rs
···
···
+
use anyhow::{Context, Result};
+
use hex;
+
use sha2::{Digest, Sha256};
+
use std::fs::{File};
+
use std::io::{BufReader, Read};
+
use std::path::Path;
+
+
const HASH_BUFFER_SIZE: usize = 8192;
+
+
pub fn calculate_image_hash(path: &Path) -> Result<String> {
+
let file = File::open(path)
+
.with_context(|| format!("Failed to open image file for hashing: {}", path.display()))?;
+
let mut reader = BufReader::with_capacity(HASH_BUFFER_SIZE, file);
+
let mut hasher = Sha256::new();
+
let mut buffer = [0u8; HASH_BUFFER_SIZE];
+
+
loop {
+
let bytes_read = reader.read(&mut buffer).with_context(|| {
+
format!("Failed to read image file for hashing: {}", path.display())
+
})?;
+
if bytes_read == 0 {
+
break;
+
}
+
hasher.update(&buffer[..bytes_read]);
+
}
+
+
let hash_bytes = hasher.finalize();
+
Ok(hex::encode(hash_bytes))
+
}