diff --git a/Cargo.lock b/Cargo.lock index 3fb91bb..76771c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,726 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + [[package]] name = "breadbox" version = "0.1.0" +dependencies = [ + "gtk4", + "gtk4-layer-shell", +] + +[[package]] +name = "cairo-rs" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc8d9aa793480744cd9a0524fef1a2e197d9eaa0f739cde19d16aba530dcb95" +dependencies = [ + "bitflags", + "cairo-sys-rs", + "glib", + "libc", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b4985713047f5faee02b8db6a6ef32bbb50269ff53c1aee716d1d195b76d54" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "cfg-expr" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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-macro", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f420376dbee041b2db374ce4573892a36222bb3f6c0c43e24f0d67eae9b646" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f31b37b1fc4b48b54f6b91b7ef04c18e00b4585d98359dd7b998774bbd91fb" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk4" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd42fdbbf48612c6e8f47c65fb92d2e8f39c25aecd6af047e83897c1a22d2a4e" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk4-sys", + "gio", + "gl", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk4-sys" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d974ac4f15e67472c3a9728daf612590b4a5762a4b33f0edd298df0b80d043c" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gio" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3848bcba3a35cc0a71df8ba8ecfd799d6bfb862342a53a4a915fb62213aa4e6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "pin-project-lite", + "smallvec", +] + +[[package]] +name = "gio-sys" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64729ba2772c080448f9f966dba8f4456beeb100d8c28a865ef8a0f2ef4987e1" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "windows-sys", +] + +[[package]] +name = "gl" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glib" +version = "0.22.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c207e04e51605dcf7b2924c41591b3a10e1438eaac5bcf448fb91f325381104a" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "smallvec", +] + +[[package]] +name = "glib-macros" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "506d23499707c7142898429757e8d9a3871d965239a2cb66dfa05052be6d6f19" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "glib-sys" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7fbac234ed5bc2a28359b7bde8e1b9cdf1441cc2d7f068e4824672d7db9445" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "gobject-sys" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a861859b887a79cf461359c192c97a57d8fb0229dd291232e57aa11f6fa72c" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "graphene-rs" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d1b7881f96869f49808b6adfe906a93a57a34204952253444d68c3208d71f1" +dependencies = [ + "glib", + "graphene-sys", + "libc", +] + +[[package]] +name = "graphene-sys" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "517f062f3fd6b7fd3e57a3f038a74b3c23ca32f51199ff028aa704609943f79c" +dependencies = [ + "glib-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gsk4" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c912dfcbd28acace5fc99c40bb9f25e1dcb73efb1f2608327f66a99acdcb62" +dependencies = [ + "cairo-rs", + "gdk4", + "glib", + "graphene-rs", + "gsk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gsk4-sys" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d54bbc7a9d8b6ffe4f0c95eede15ccfb365c8bf521275abe6bcfb57b18fb8a" +dependencies = [ + "cairo-sys-rs", + "gdk4-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk4" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7181b837f04cbe93f79441475f7a00560a92cba7a72e38cc1a68b6f8b78eaae2" +dependencies = [ + "cairo-rs", + "field-offset", + "futures-channel", + "gdk-pixbuf", + "gdk4", + "gio", + "glib", + "graphene-rs", + "gsk4", + "gtk4-macros", + "gtk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gtk4-layer-shell" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4069987ff4793699511a251028cc336b438e46565b463f111250148d574752a" +dependencies = [ + "bitflags", + "gdk4", + "glib", + "glib-sys", + "gtk4", + "gtk4-layer-shell-sys", + "libc", +] + +[[package]] +name = "gtk4-layer-shell-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f566a5ec5bcc454e7fcf2ab76930887ced5365afce12c1e5201bb296b95f1b9" +dependencies = [ + "gdk4-sys", + "glib-sys", + "gtk4-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk4-macros" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3581b242ba62fdff122ebb626ea641582ec326031622bd19d60f85029c804a87" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "gtk4-sys" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ba8e695e2640455561274e65e45f0a151619e450746007667f4b23ceae4e1b" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "gsk4-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +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 = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "pango" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "251bdc6e6487b811be0e406a21e301e07e45c0aa8fa39e00c0c8e12a91752438" +dependencies = [ + "gio", + "glib", + "libc", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd111a20ca90fedf03e09c59783c679c00900f1d8491cca5399f5e33609d5d6" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[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.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +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_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "system-deps" +version = "7.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[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 = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" diff --git a/Cargo.toml b/Cargo.toml index bf8e14a..7e0d3ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,3 +2,7 @@ name = "breadbox" version = "0.1.0" edition = "2021" + +[dependencies] +gtk4 = "0.11" +gtk4-layer-shell = "0.8" diff --git a/src/main.rs b/src/main.rs index ad419bc..52c2a21 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,17 +8,62 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; +use gtk4::{ + gdk::Display, + glib, + pango::EllipsizeMode, + prelude::*, + Application, ApplicationWindow, Box as GBox, CssProvider, EventControllerKey, Label, + ListBox, Orientation, PolicyType, ScrolledWindow, SearchEntry, SelectionMode, +}; +use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell}; + const CACHE_TIMEOUT_SECS: u64 = 86400; +const CSS: &str = " +window, .background { + background-color: #1e1e2e; +} +searchentry { + background-color: #313244; + color: #cdd6f4; + caret-color: #cba6f7; + border: none; + outline: none; + box-shadow: none; + padding: 12px 16px; + font-size: 15px; +} +listbox { + background-color: transparent; + padding: 4px; +} +row { + padding: 6px 12px; + color: #cdd6f4; + background-color: transparent; + border-radius: 4px; +} +row:selected { + background-color: #45475a; +} +.action { + color: #6c7086; + font-size: 12px; +} +"; + +// ---- cache helpers -------------------------------------------------------- + fn home_dir() -> PathBuf { PathBuf::from(env::var("HOME").unwrap_or_else(|_| "/tmp".into())) } fn cache_path() -> PathBuf { - let dir = env::var("XDG_CACHE_HOME") + env::var("XDG_CACHE_HOME") .map(PathBuf::from) - .unwrap_or_else(|_| home_dir().join(".cache")); - dir.join("breadbox.cache") + .unwrap_or_else(|_| home_dir().join(".cache")) + .join("breadbox.cache") } fn app_dirs() -> [PathBuf; 2] { @@ -41,10 +86,8 @@ fn cache_valid(cache: &Path) -> bool { .unwrap_or_default() .as_secs(); let cm = mtime(cache); - if now.saturating_sub(cm) >= CACHE_TIMEOUT_SECS { - return false; - } - app_dirs().iter().all(|d| !d.is_dir() || mtime(d) <= cm) + now.saturating_sub(cm) < CACHE_TIMEOUT_SECS + && app_dirs().iter().all(|d| !d.is_dir() || mtime(d) <= cm) } fn strip_exec_codes(exec: &str) -> String { @@ -69,13 +112,13 @@ fn strip_exec_codes(exec: &str) -> String { out } -struct App { +struct DesktopApp { name: String, exec: String, terminal: bool, } -fn parse_desktop(path: &Path) -> Option { +fn parse_desktop(path: &Path) -> Option { let file = File::open(path).ok()?; let mut in_entry = false; let (mut name, mut exec, mut app_type) = (None::, None::, None::); @@ -123,12 +166,12 @@ fn parse_desktop(path: &Path) -> Option { return None; } - Some(App { name, exec, terminal }) + Some(DesktopApp { name, exec, terminal }) } fn build_cache(cache: &Path) { let _ = fs::create_dir_all(cache.parent().unwrap_or(Path::new("/tmp"))); - let mut apps: HashMap = HashMap::new(); + let mut apps: HashMap = HashMap::new(); for dir in &app_dirs() { let Ok(entries) = fs::read_dir(dir) else { continue }; @@ -137,9 +180,8 @@ fn build_cache(cache: &Path) { if path.extension().and_then(|e| e.to_str()) != Some("desktop") { continue; } - let id = entry.file_name().to_string_lossy().into_owned(); if let Some(app) = parse_desktop(&path) { - apps.insert(id, app); + apps.insert(entry.file_name().to_string_lossy().into_owned(), app); } } } @@ -162,6 +204,21 @@ fn build_cache(cache: &Path) { } } +fn load_entries(cache: &Path) -> Vec<(String, String)> { + fs::read_to_string(cache) + .unwrap_or_default() + .lines() + .filter_map(|line| { + let mut parts = line.splitn(2, '\t'); + let name = parts.next()?.to_string(); + let action = parts.next()?.to_string(); + (!name.is_empty() && !action.is_empty()).then_some((name, action)) + }) + .collect() +} + +// ---- launch --------------------------------------------------------------- + fn pick_terminal() -> String { if let Ok(t) = env::var("TERMINAL") { if !t.is_empty() { @@ -177,83 +234,7 @@ fn pick_terminal() -> String { "xterm".to_string() } -fn main() { - let cache = cache_path(); - - if env::var("BREADBOX_REBUILD_ONLY").as_deref() == Ok("1") { - build_cache(&cache); - return; - } - - // Toggle: second press closes an open wofi instance - if Command::new("pgrep") - .args(["-f", "wofi.*breadbox"]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .map(|s| s.success()) - .unwrap_or(false) - { - let _ = Command::new("pkill") - .args(["-f", "wofi.*breadbox"]) - .status(); - return; - } - - // Stale-while-revalidate: never block on a rebuild if cache exists - if !cache.exists() { - build_cache(&cache); - } else if !cache_valid(&cache) { - if let Ok(exe) = env::current_exe() { - let _ = Command::new(exe) - .env("BREADBOX_REBUILD_ONLY", "1") - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn(); - } - } - - let content = fs::read_to_string(&cache).unwrap_or_default(); - - let mut child = match Command::new("wofi") - .args([ - "--dmenu", - "--parse-search", - "--matching", - "fuzzy", - "--insensitive", - "--prompt", - "breadbox", - ]) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::null()) - .spawn() - { - Ok(c) => c, - Err(_) => return, - }; - - if let Some(mut stdin) = child.stdin.take() { - let _ = write!(stdin, "{}", content); - } - - let out = match child.wait_with_output() { - Ok(o) => o, - Err(_) => return, - }; - - let choice = std::str::from_utf8(&out.stdout) - .unwrap_or("") - .trim() - .to_string(); - if choice.is_empty() { - return; - } - - let action = choice.split('\t').nth(1).unwrap_or(""); - +fn do_launch(action: &str) { if let Some(cmd) = action.strip_prefix("app::") { let _ = Command::new("bash") .args(["-c", cmd]) @@ -271,3 +252,265 @@ fn main() { .spawn(); } } + +// ---- fuzzy matching ------------------------------------------------------- + +fn fuzzy_matches(pattern: &str, text: &str) -> bool { + if pattern.is_empty() { + return true; + } + let mut chars = text.chars(); + for pc in pattern.chars() { + let pl = pc.to_lowercase().next().unwrap_or(pc); + if !chars + .by_ref() + .any(|tc| tc.to_lowercase().next().unwrap_or(tc) == pl) + { + return false; + } + } + true +} + +// ---- toggle via pid file -------------------------------------------------- + +fn pid_file() -> PathBuf { + env::var("XDG_RUNTIME_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from("/tmp")) + .join("breadbox.pid") +} + +// Returns false if an existing instance was killed (caller should exit). +fn toggle_or_continue() -> bool { + let pf = pid_file(); + if let Ok(content) = fs::read_to_string(&pf) { + if let Ok(pid) = content.trim().parse::() { + if Path::new(&format!("/proc/{}", pid)).exists() { + let _ = Command::new("kill").arg(pid.to_string()).status(); + return false; + } + } + } + let _ = fs::write(&pf, std::process::id().to_string()); + true +} + +fn cleanup_pid() { + let _ = fs::remove_file(pid_file()); +} + +// ---- UI ------------------------------------------------------------------- + +fn get_row_data(row: >k4::ListBoxRow, key: &str) -> String { + unsafe { + row.data::(key) + .map(|p| p.as_ref().clone()) + .unwrap_or_default() + } +} + +fn run_ui(entries: Vec<(String, String)>) { + let app = Application::builder() + .application_id("com.breadway.breadbox") + .build(); + + app.connect_activate(move |app| { + let provider = CssProvider::new(); + provider.load_from_data(CSS); + gtk4::style_context_add_provider_for_display( + &Display::default().expect("no display"), + &provider, + gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION, + ); + + let window = ApplicationWindow::builder() + .application(app) + .default_width(700) + .build(); + + window.init_layer_shell(); + window.set_layer(Layer::Overlay); + window.set_keyboard_mode(KeyboardMode::OnDemand); + window.set_anchor(Edge::Top, true); + window.set_exclusive_zone(-1); + + let vbox = GBox::new(Orientation::Vertical, 0); + + let search = SearchEntry::new(); + search.set_placeholder_text(Some("breadbox")); + vbox.append(&search); + + let scroll = ScrolledWindow::new(); + scroll.set_policy(PolicyType::Never, PolicyType::Automatic); + scroll.set_max_content_height(400); + scroll.set_propagate_natural_height(true); + + let list = ListBox::new(); + list.set_selection_mode(SelectionMode::Browse); + + for (name, action) in &entries { + let row = gtk4::ListBoxRow::new(); + let hbox = GBox::new(Orientation::Horizontal, 8); + hbox.set_margin_start(4); + hbox.set_margin_end(4); + + let name_lbl = Label::new(Some(name)); + name_lbl.set_xalign(0.0); + name_lbl.set_hexpand(true); + hbox.append(&name_lbl); + + let action_lbl = Label::new(Some(action)); + action_lbl.add_css_class("action"); + action_lbl.set_xalign(1.0); + action_lbl.set_ellipsize(EllipsizeMode::End); + action_lbl.set_max_width_chars(50); + hbox.append(&action_lbl); + + row.set_child(Some(&hbox)); + unsafe { + row.set_data("name", name.clone()); + row.set_data("action", action.clone()); + } + list.append(&row); + } + + if let Some(first) = list.row_at_index(0) { + list.select_row(Some(&first)); + } + + scroll.set_child(Some(&list)); + vbox.append(&scroll); + window.set_child(Some(&vbox)); + + // Filter rows on every keystroke + let list_f = list.clone(); + search.connect_changed(move |entry| { + let text = entry.text(); + let query = text.as_str(); + let mut first_vis: Option = None; + let mut i = 0i32; + while let Some(row) = list_f.row_at_index(i) { + let name = get_row_data(&row, "name"); + let vis = fuzzy_matches(query, &name); + row.set_visible(vis); + if vis && first_vis.is_none() { + first_vis = Some(row); + } + i += 1; + } + list_f.select_row(first_vis.as_ref()); + }); + + // Keyboard: Esc, Enter, arrows — keep focus in search bar + let key_ctrl = EventControllerKey::new(); + let window_k = window.clone(); + let list_k = list.clone(); + key_ctrl.connect_key_pressed(move |_, key, _, _| { + use gtk4::gdk::Key; + match key { + Key::Escape => { + cleanup_pid(); + window_k.close(); + glib::Propagation::Stop + } + Key::Return | Key::KP_Enter => { + if let Some(row) = list_k.selected_row() { + let action = get_row_data(&row, "action"); + if !action.is_empty() { + do_launch(&action); + cleanup_pid(); + window_k.close(); + } + } + glib::Propagation::Stop + } + Key::Down => { + let cur = list_k.selected_row().map(|r| r.index()).unwrap_or(-1); + let mut i = cur + 1; + loop { + match list_k.row_at_index(i) { + Some(r) if r.is_visible() => { + list_k.select_row(Some(&r)); + break; + } + Some(_) => i += 1, + None => break, + } + } + glib::Propagation::Stop + } + Key::Up => { + let cur = list_k.selected_row().map(|r| r.index()).unwrap_or(0); + let mut i = cur - 1; + loop { + if i < 0 { + break; + } + match list_k.row_at_index(i) { + Some(r) if r.is_visible() => { + list_k.select_row(Some(&r)); + break; + } + Some(_) => i -= 1, + None => break, + } + } + glib::Propagation::Stop + } + _ => glib::Propagation::Proceed, + } + }); + search.add_controller(key_ctrl); + + // Click to launch + let window_a = window.clone(); + list.connect_row_activated(move |_, row| { + let action = get_row_data(row, "action"); + if !action.is_empty() { + do_launch(&action); + cleanup_pid(); + window_a.close(); + } + }); + + // Cleanup pid when window is destroyed for any reason + window.connect_destroy(|_| cleanup_pid()); + + window.present(); + search.grab_focus(); + }); + + app.run(); +} + +// ---- main ----------------------------------------------------------------- + +fn main() { + let cache = cache_path(); + + if env::var("BREADBOX_REBUILD_ONLY").as_deref() == Ok("1") { + build_cache(&cache); + return; + } + + if !toggle_or_continue() { + return; + } + + if !cache.exists() { + build_cache(&cache); + } else if !cache_valid(&cache) { + if let Ok(exe) = env::current_exe() { + let _ = Command::new(exe) + .env("BREADBOX_REBUILD_ONLY", "1") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn(); + } + } + + let entries = load_entries(&cache); + run_ui(entries); +}