Initial commit

This commit is contained in:
Breadway 2026-07-02 20:59:24 +08:00
commit 854c5752f9
10 changed files with 1614 additions and 0 deletions

31
.gitignore vendored Normal file
View file

@ -0,0 +1,31 @@
# Rust build artifacts
target/
# Editor and IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# OS artifacts
.DS_Store
Thumbs.db
desktop.ini
# Environment and secrets
.env
.env.*
*.env
*.pem
*.key
*.p12
secrets/
# Log files
*.log
logs/
# Runtime files
*.sock
*.pid

903
Cargo.lock generated Normal file
View file

@ -0,0 +1,903 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.61.2",
]
[[package]]
name = "anyhow"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "breadshot"
version = "1.0.0"
dependencies = [
"anyhow",
"chrono",
"clap",
"dirs",
"serde",
"serde_json",
"toml",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "bumpalo"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "cc"
version = "1.2.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chrono"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "clap"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "dirs"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.48.0",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-task"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "iana-time-zone"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "indexmap"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "js-sys"
version = "0.3.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
dependencies = [
"cfg-if",
"futures-util",
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libredox"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3"
dependencies = [
"libc",
]
[[package]]
name = "log"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "matchers"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata",
]
[[package]]
name = "memchr"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_users"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [
"getrandom",
"libredox",
"thiserror",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "serde_spanned"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[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]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "thread_local"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [
"cfg-if",
]
[[package]]
name = "toml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"toml_write",
"winnow",
]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "tracing"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasm-bindgen"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
dependencies = [
"unicode-ident",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
"memchr",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"

23
Cargo.toml Normal file
View file

@ -0,0 +1,23 @@
[package]
name = "breadshot"
version = "1.0.0"
edition = "2021"
license = "MIT"
authors = ["Breadway"]
description = "Screenshot utility for the bread ecosystem"
[dependencies]
anyhow = "1"
chrono = "0.4"
clap = { version = "4", features = ["derive"] }
dirs = "5"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "0.8"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[profile.release]
lto = "thin"
codegen-units = 1
strip = "symbols"

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Breadway
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

14
Makefile Normal file
View file

@ -0,0 +1,14 @@
PREFIX ?= /usr/local
BINDIR := $(PREFIX)/bin
TARGET := target/release/breadshot
.PHONY: build install uninstall
build:
cargo build --release
install: build
install -Dm755 $(TARGET) $(DESTDIR)$(BINDIR)/breadshot
uninstall:
rm -f $(DESTDIR)$(BINDIR)/breadshot

98
README.md Normal file
View file

@ -0,0 +1,98 @@
# breadshot
Wayland screenshot utility for the bread ecosystem. Wraps `grim`, `slurp`, and `wl-copy` with Hyprland-aware geometry resolution, clipboard integration, and desktop notifications.
## Requirements
Required (must be in `$PATH`):
- `grim` — Wayland screenshot tool
- `slurp` — region/window selection
- `wl-copy` (wl-clipboard) — clipboard write
- `hyprctl` — Hyprland IPC (for window and monitor geometry)
Optional:
- `hyprpicker` — screen freeze during selection (`--freeze`)
- `notify-send` — desktop notifications (silently skipped if absent)
## Build and install
```sh
# build only
cargo build --release
# build and install to /usr/local/bin
make install
# install to a custom prefix
make install PREFIX=/usr
```
## Usage
```
breadshot <mode> [options]
```
### Modes
| Mode | Description |
|---|---|
| `region` | Interactive region selection |
| `window` | Click to select a window on the active workspaces |
| `output` | Click to select a monitor |
| `active-window` | Capture the currently focused window |
| `active-output` | Capture the monitor containing the active workspace |
### Options
| Flag | Short | Description |
|---|---|---|
| `--clipboard-only` | `-c` | Copy to clipboard only, do not save to disk |
| `--silent` | `-s` | Suppress notifications |
| `--freeze` | `-z` | Freeze screen during selection (requires `hyprpicker`) |
| `--output-dir <DIR>` | `-o` | Override the save directory from config |
| `--filename <NAME>` | `-f` | Override the output filename (without path) |
| `--config <FILE>` | | Use a specific config file |
### Examples
```sh
# interactive region selection, save and copy
breadshot region
# capture active window to clipboard only
breadshot active-window --clipboard-only
# region selection with screen frozen, saved to a custom path
breadshot region --freeze --output-dir ~/Desktop --filename capture.png
```
## Configuration
Config file: `~/.config/breadshot/config.toml`
All keys are optional. Missing keys fall back to the defaults shown below.
```toml
# Directory where screenshots are saved
save_dir = "~/Pictures/Screenshots"
# Suppress notifications globally
silent = false
# Freeze screen during selection by default (requires hyprpicker)
freeze = false
# Notification display duration in milliseconds
notif_timeout = 5000
# strftime format used to generate filenames
# Filename pattern: <date_format>_breadshot.png
date_format = "%Y-%m-%d-%H%M%S"
```
## License
MIT — see [LICENSE](LICENSE).

12
bakery.toml Normal file
View file

@ -0,0 +1,12 @@
name = "breadshot"
description = "Wayland screenshot utility for the bread ecosystem — wraps grim/slurp/wl-copy with Hyprland-aware geometry"
binaries = ["breadshot"]
system_deps = ["grim", "slurp", "wl-clipboard"]
optional_system_deps = ["hyprland", "hyprpicker", "libnotify"]
bread_deps = []
[config]
dir = "~/.config/breadshot"
[install]
post_install = []

389
src/capture.rs Normal file
View file

@ -0,0 +1,389 @@
use anyhow::{bail, Context, Result};
use clap::ValueEnum;
use serde_json::Value;
use std::{
io::Write,
path::{Path, PathBuf},
process::{Command, Stdio},
time::Duration,
};
use crate::config::Config;
#[derive(Debug, Clone, ValueEnum)]
pub enum Mode {
/// Select a region interactively
Region,
/// Click to select a window
Window,
/// Click to select a monitor
Output,
/// Capture the active window
#[value(name = "active-window")]
ActiveWindow,
/// Capture the active monitor
#[value(name = "active-output")]
ActiveOutput,
}
pub struct Overrides {
pub clipboard_only: bool,
pub silent: bool,
pub freeze: bool,
pub output_dir: Option<PathBuf>,
pub filename: Option<String>,
}
pub fn run(mode: &Mode, config: &Config, overrides: Overrides) -> Result<()> {
check_deps()?;
let save_dir = overrides.output_dir.as_ref().unwrap_or(&config.save_dir);
let filename = overrides.filename.unwrap_or_else(|| {
format!(
"{}_breadshot.png",
chrono::Local::now().format(&config.date_format)
)
});
let save_path = save_dir.join(&filename);
let silent = overrides.silent || config.silent;
let freeze = overrides.freeze || config.freeze;
let clipboard_only = overrides.clipboard_only;
let _freeze_guard = if freeze {
FreezeGuard::try_spawn()
.map_err(|e| tracing::warn!("freeze: {e}"))
.ok()
} else {
None
};
let geometry = geometry_for_mode(mode)?;
tracing::debug!("geometry: {geometry}");
if clipboard_only {
copy_only(&geometry)?;
} else {
std::fs::create_dir_all(save_dir)
.with_context(|| format!("creating {}", save_dir.display()))?;
save_and_copy(&geometry, &save_path)?;
}
if !silent {
let msg = if clipboard_only {
"Copied to clipboard".to_string()
} else {
format!("Saved to <i>{}</i>", save_path.display())
};
send_notification("Screenshot", &msg, config.notif_timeout, &save_path);
}
Ok(())
}
// --- geometry ---
fn geometry_for_mode(mode: &Mode) -> Result<String> {
match mode {
Mode::Region => geometry_region(),
Mode::Window => geometry_window().and_then(|g| trim_geometry(&g)),
Mode::Output => geometry_output(),
Mode::ActiveWindow => geometry_active_window().and_then(|g| trim_geometry(&g)),
Mode::ActiveOutput => geometry_active_output(),
}
}
fn geometry_region() -> Result<String> {
slurp(&["-d"])
}
fn geometry_output() -> Result<String> {
slurp(&["-or"])
}
fn geometry_window() -> Result<String> {
let monitors = hyprctl_json("monitors")?;
let active_ids: Vec<i64> = monitors
.as_array()
.unwrap_or(&vec![])
.iter()
.filter_map(|m| m["activeWorkspace"]["id"].as_i64())
.collect();
let clients = hyprctl_json("clients")?;
let boxes: Vec<String> = clients
.as_array()
.unwrap_or(&vec![])
.iter()
.filter(|c| {
c["workspace"]["id"]
.as_i64()
.is_some_and(|id| active_ids.contains(&id))
})
.map(|c| {
format!(
"{},{} {}x{}",
c["at"][0].as_i64().unwrap_or(0),
c["at"][1].as_i64().unwrap_or(0),
c["size"][0].as_i64().unwrap_or(0),
c["size"][1].as_i64().unwrap_or(0),
)
})
.collect();
slurp_piped(&["-r"], &boxes.join("\n"))
}
fn geometry_active_window() -> Result<String> {
let w = hyprctl_json("activewindow")?;
Ok(format!(
"{},{} {}x{}",
w["at"][0].as_i64().context("missing at[0]")?,
w["at"][1].as_i64().context("missing at[1]")?,
w["size"][0].as_i64().context("missing size[0]")?,
w["size"][1].as_i64().context("missing size[1]")?,
))
}
fn geometry_active_output() -> Result<String> {
let active_id = hyprctl_json("activeworkspace")?["id"]
.as_i64()
.context("no active workspace id")?;
let monitors = hyprctl_json("monitors")?;
let m = monitors
.as_array()
.and_then(|arr| {
arr.iter()
.find(|m| m["activeWorkspace"]["id"].as_i64() == Some(active_id))
})
.context("no monitor for active workspace")?;
monitor_geometry(m)
}
fn monitor_geometry(m: &Value) -> Result<String> {
let x = m["x"].as_i64().unwrap_or(0);
let y = m["y"].as_i64().unwrap_or(0);
let w = m["width"].as_f64().context("missing width")?;
let h = m["height"].as_f64().context("missing height")?;
let scale = m["scale"].as_f64().unwrap_or(1.0);
Ok(format!(
"{x},{y} {}x{}",
(w / scale).round() as i64,
(h / scale).round() as i64
))
}
// Clips window geometry to the logical bounds of the monitor layout.
// hyprctl clients returns logical (scaled) coordinates, but a window
// can technically extend outside the visible area.
fn trim_geometry(geometry: &str) -> Result<String> {
let (xy, wh) = geometry.split_once(' ').context("invalid geometry")?;
let (xs, ys) = xy.split_once(',').context("invalid xy")?;
let (ws, hs) = wh.split_once('x').context("invalid wh")?;
let x: i64 = xs.trim().parse().context("invalid x")?;
let y: i64 = ys.trim().parse().context("invalid y")?;
let w: i64 = ws.trim().parse().context("invalid w")?;
let h: i64 = hs.trim().parse().context("invalid h")?;
let monitors = hyprctl_json("monitors")?;
let monitors = monitors.as_array().context("monitors not an array")?;
let (max_x, max_y, min_x, min_y) =
monitors
.iter()
.fold((i64::MIN, i64::MIN, i64::MAX, i64::MAX), |acc, m| {
let mx = m["x"].as_i64().unwrap_or(0);
let my = m["y"].as_i64().unwrap_or(0);
let mw = m["width"].as_f64().unwrap_or(0.0);
let mh = m["height"].as_f64().unwrap_or(0.0);
let scale = m["scale"].as_f64().unwrap_or(1.0);
let transform = m["transform"].as_i64().unwrap_or(0);
let (lw, lh) = if transform % 2 == 0 {
((mw / scale).round() as i64, (mh / scale).round() as i64)
} else {
((mh / scale).round() as i64, (mw / scale).round() as i64)
};
(
acc.0.max(mx + lw),
acc.1.max(my + lh),
acc.2.min(mx),
acc.3.min(my),
)
});
let mut cx = x;
let mut cy = y;
let mut cw = w;
let mut ch = h;
if x + w > max_x {
cw = max_x - x;
}
if y + h > max_y {
ch = max_y - y;
}
if x < min_x {
cx = min_x;
cw += x - min_x;
}
if y < min_y {
cy = min_y;
ch += y - min_y;
}
Ok(format!("{cx},{cy} {cw}x{ch}"))
}
// --- capture ---
fn copy_only(geometry: &str) -> Result<()> {
let mut grim = Command::new("grim")
.args(["-g", geometry, "-"])
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.context("spawning grim")?;
let stdout = grim.stdout.take().context("grim stdout")?;
let mut wl_copy = Command::new("wl-copy")
.args(["--type", "image/png"])
.stdin(stdout)
.spawn()
.context("spawning wl-copy")?;
grim.wait().context("waiting for grim")?;
wl_copy.wait().context("waiting for wl-copy")?;
Ok(())
}
fn save_and_copy(geometry: &str, path: &Path) -> Result<()> {
let status = Command::new("grim")
.args(["-g", geometry, path.to_str().unwrap_or("")])
.status()
.context("running grim")?;
if !status.success() {
bail!("grim exited with {status}");
}
let data = std::fs::read(path).with_context(|| format!("reading {}", path.display()))?;
let mut wl_copy = Command::new("wl-copy")
.args(["--type", "image/png"])
.stdin(Stdio::piped())
.spawn()
.context("spawning wl-copy")?;
wl_copy
.stdin
.take()
.context("wl-copy stdin")?
.write_all(&data)
.context("piping to wl-copy")?;
wl_copy.wait().context("waiting for wl-copy")?;
Ok(())
}
fn send_notification(title: &str, msg: &str, timeout: u32, path: &Path) {
let mut cmd = Command::new("notify-send");
cmd.args([title, msg, "-t", &timeout.to_string(), "-a", "breadshot"]);
if path.exists() {
cmd.args(["-i", path.to_str().unwrap_or("")]);
}
if let Err(e) = cmd.status() {
tracing::warn!("notify-send: {e}");
}
}
// --- helpers ---
fn hyprctl_json(subcmd: &str) -> Result<Value> {
let out = Command::new("hyprctl")
.args(["-j", subcmd])
.output()
.context("running hyprctl")?;
serde_json::from_slice(&out.stdout)
.with_context(|| format!("parsing hyprctl {subcmd} output"))
}
fn slurp(args: &[&str]) -> Result<String> {
let out = Command::new("slurp")
.args(args)
.output()
.context("running slurp")?;
if !out.status.success() {
bail!("selection cancelled");
}
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
}
fn slurp_piped(args: &[&str], stdin_data: &str) -> Result<String> {
let mut child = Command::new("slurp")
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.context("spawning slurp")?;
child
.stdin
.take()
.context("slurp stdin")?
.write_all(stdin_data.as_bytes())
.context("writing to slurp")?;
let out = child.wait_with_output().context("waiting for slurp")?;
if !out.status.success() {
bail!("selection cancelled");
}
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
}
fn in_path(cmd: &str) -> bool {
std::env::var_os("PATH")
.map(|path| std::env::split_paths(&path).any(|dir| dir.join(cmd).exists()))
.unwrap_or(false)
}
fn check_deps() -> Result<()> {
let required = ["grim", "slurp", "wl-copy", "hyprctl"];
let missing: Vec<_> = required.iter().copied().filter(|&d| !in_path(d)).collect();
if !missing.is_empty() {
bail!("missing required tools: {}", missing.join(", "));
}
Ok(())
}
// --- freeze ---
struct FreezeGuard {
child: std::process::Child,
}
impl FreezeGuard {
fn try_spawn() -> Result<Self> {
if !in_path("hyprpicker") {
bail!("hyprpicker not installed");
}
let child = Command::new("hyprpicker")
.args(["-r", "-z"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.context("spawning hyprpicker")?;
std::thread::sleep(Duration::from_millis(200));
Ok(Self { child })
}
}
impl Drop for FreezeGuard {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}

51
src/config.rs Normal file
View file

@ -0,0 +1,51 @@
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
pub save_dir: PathBuf,
pub silent: bool,
pub freeze: bool,
pub notif_timeout: u32,
pub date_format: String,
}
impl Default for Config {
fn default() -> Self {
Self {
save_dir: dirs::picture_dir()
.unwrap_or_else(|| dirs::home_dir().unwrap_or_default())
.join("Screenshots"),
silent: false,
freeze: false,
notif_timeout: 5000,
date_format: "%Y-%m-%d-%H%M%S".to_string(),
}
}
}
impl Config {
pub fn load() -> Result<Self> {
Self::load_from(&default_path())
}
pub fn load_from(path: &Path) -> Result<Self> {
if !path.exists() {
tracing::debug!("no config at {}, using defaults", path.display());
return Ok(Self::default());
}
let content = std::fs::read_to_string(path)
.with_context(|| format!("reading {}", path.display()))?;
toml::from_str(&content)
.with_context(|| format!("parsing {}", path.display()))
}
}
pub fn default_path() -> PathBuf {
dirs::config_dir()
.unwrap_or_else(|| dirs::home_dir().unwrap_or_default().join(".config"))
.join("breadshot")
.join("config.toml")
}

72
src/main.rs Normal file
View file

@ -0,0 +1,72 @@
mod capture;
mod config;
use anyhow::Result;
use clap::Parser;
use std::path::PathBuf;
use tracing_subscriber::EnvFilter;
use capture::{Mode, Overrides};
use config::Config;
#[derive(Parser)]
#[command(
name = "breadshot",
version,
about = "Screenshot utility for the bread ecosystem",
disable_help_subcommand = true,
)]
struct Cli {
/// Capture mode
mode: Mode,
/// Copy to clipboard only, don't save to disk
#[arg(long, short = 'c')]
clipboard_only: bool,
/// Suppress notifications
#[arg(long, short = 's')]
silent: bool,
/// Freeze screen during selection (requires hyprpicker)
#[arg(long, short = 'z')]
freeze: bool,
/// Override save directory from config
#[arg(long, short = 'o', value_name = "DIR")]
output_dir: Option<PathBuf>,
/// Override output filename (without path)
#[arg(long, short = 'f', value_name = "NAME")]
filename: Option<String>,
/// Path to config file
#[arg(long, value_name = "FILE")]
config: Option<PathBuf>,
}
fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.without_time()
.init();
let cli = Cli::parse();
let config = match &cli.config {
Some(path) => Config::load_from(path)?,
None => Config::load()?,
};
capture::run(
&cli.mode,
&config,
Overrides {
clipboard_only: cli.clipboard_only,
silent: cli.silent,
freeze: cli.freeze,
output_dir: cli.output_dir,
filename: cli.filename,
},
)
}