From 854c5752f9fb0bb123d5736237f75db5ad0621f8 Mon Sep 17 00:00:00 2001 From: Breadway Date: Thu, 2 Jul 2026 20:59:24 +0800 Subject: [PATCH] Initial commit --- .gitignore | 31 ++ Cargo.lock | 903 +++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 23 ++ LICENSE | 21 ++ Makefile | 14 + README.md | 98 ++++++ bakery.toml | 12 + src/capture.rs | 389 +++++++++++++++++++++ src/config.rs | 51 +++ src/main.rs | 72 ++++ 10 files changed, 1614 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 bakery.toml create mode 100644 src/capture.rs create mode 100644 src/config.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..36f7f5b --- /dev/null +++ b/.gitignore @@ -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 diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..8dbb11a --- /dev/null +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5df1443 --- /dev/null +++ b/Cargo.toml @@ -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" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..373e4ee --- /dev/null +++ b/LICENSE @@ -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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8d1a3e0 --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..89cbf0f --- /dev/null +++ b/README.md @@ -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 [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 ` | `-o` | Override the save directory from config | +| `--filename ` | `-f` | Override the output filename (without path) | +| `--config ` | | 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: _breadshot.png +date_format = "%Y-%m-%d-%H%M%S" +``` + +## License + +MIT — see [LICENSE](LICENSE). diff --git a/bakery.toml b/bakery.toml new file mode 100644 index 0000000..616d788 --- /dev/null +++ b/bakery.toml @@ -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 = [] diff --git a/src/capture.rs b/src/capture.rs new file mode 100644 index 0000000..08dda97 --- /dev/null +++ b/src/capture.rs @@ -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, + pub filename: Option, +} + +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 {}", save_path.display()) + }; + send_notification("Screenshot", &msg, config.notif_timeout, &save_path); + } + + Ok(()) +} + +// --- geometry --- + +fn geometry_for_mode(mode: &Mode) -> Result { + 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 { + slurp(&["-d"]) +} + +fn geometry_output() -> Result { + slurp(&["-or"]) +} + +fn geometry_window() -> Result { + let monitors = hyprctl_json("monitors")?; + let active_ids: Vec = monitors + .as_array() + .unwrap_or(&vec![]) + .iter() + .filter_map(|m| m["activeWorkspace"]["id"].as_i64()) + .collect(); + + let clients = hyprctl_json("clients")?; + let boxes: Vec = 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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(); + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..8f8ffd8 --- /dev/null +++ b/src/config.rs @@ -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::load_from(&default_path()) + } + + pub fn load_from(path: &Path) -> Result { + 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") +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..8e79de6 --- /dev/null +++ b/src/main.rs @@ -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, + + /// Override output filename (without path) + #[arg(long, short = 'f', value_name = "NAME")] + filename: Option, + + /// Path to config file + #[arg(long, value_name = "FILE")] + config: Option, +} + +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, + }, + ) +}