breadbox: replace wofi with native GTK4 layer-shell UI

Remove the wofi subprocess entirely. The launcher now renders its own
Wayland overlay window via gtk4 + gtk4-layer-shell: a SearchEntry at
the top with live fuzzy filtering, a ListBox of results below, and
keyboard navigation (Enter/Esc/arrows). Toggle (keybind press while
open closes it) is handled via a PID file in $XDG_RUNTIME_DIR.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Breadway 2026-05-23 11:36:29 +08:00
parent d94a00d982
commit 30e40ec54f
3 changed files with 1057 additions and 90 deletions

720
Cargo.lock generated
View file

@ -2,6 +2,726 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 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]] [[package]]
name = "breadbox" name = "breadbox"
version = "0.1.0" 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"

View file

@ -2,3 +2,7 @@
name = "breadbox" name = "breadbox"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies]
gtk4 = "0.11"
gtk4-layer-shell = "0.8"

View file

@ -8,17 +8,62 @@ use std::{
time::{SystemTime, UNIX_EPOCH}, 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 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 { fn home_dir() -> PathBuf {
PathBuf::from(env::var("HOME").unwrap_or_else(|_| "/tmp".into())) PathBuf::from(env::var("HOME").unwrap_or_else(|_| "/tmp".into()))
} }
fn cache_path() -> PathBuf { fn cache_path() -> PathBuf {
let dir = env::var("XDG_CACHE_HOME") env::var("XDG_CACHE_HOME")
.map(PathBuf::from) .map(PathBuf::from)
.unwrap_or_else(|_| home_dir().join(".cache")); .unwrap_or_else(|_| home_dir().join(".cache"))
dir.join("breadbox.cache") .join("breadbox.cache")
} }
fn app_dirs() -> [PathBuf; 2] { fn app_dirs() -> [PathBuf; 2] {
@ -41,10 +86,8 @@ fn cache_valid(cache: &Path) -> bool {
.unwrap_or_default() .unwrap_or_default()
.as_secs(); .as_secs();
let cm = mtime(cache); let cm = mtime(cache);
if now.saturating_sub(cm) >= CACHE_TIMEOUT_SECS { now.saturating_sub(cm) < CACHE_TIMEOUT_SECS
return false; && app_dirs().iter().all(|d| !d.is_dir() || mtime(d) <= cm)
}
app_dirs().iter().all(|d| !d.is_dir() || mtime(d) <= cm)
} }
fn strip_exec_codes(exec: &str) -> String { fn strip_exec_codes(exec: &str) -> String {
@ -69,13 +112,13 @@ fn strip_exec_codes(exec: &str) -> String {
out out
} }
struct App { struct DesktopApp {
name: String, name: String,
exec: String, exec: String,
terminal: bool, terminal: bool,
} }
fn parse_desktop(path: &Path) -> Option<App> { fn parse_desktop(path: &Path) -> Option<DesktopApp> {
let file = File::open(path).ok()?; let file = File::open(path).ok()?;
let mut in_entry = false; let mut in_entry = false;
let (mut name, mut exec, mut app_type) = (None::<String>, None::<String>, None::<String>); let (mut name, mut exec, mut app_type) = (None::<String>, None::<String>, None::<String>);
@ -123,12 +166,12 @@ fn parse_desktop(path: &Path) -> Option<App> {
return None; return None;
} }
Some(App { name, exec, terminal }) Some(DesktopApp { name, exec, terminal })
} }
fn build_cache(cache: &Path) { fn build_cache(cache: &Path) {
let _ = fs::create_dir_all(cache.parent().unwrap_or(Path::new("/tmp"))); let _ = fs::create_dir_all(cache.parent().unwrap_or(Path::new("/tmp")));
let mut apps: HashMap<String, App> = HashMap::new(); let mut apps: HashMap<String, DesktopApp> = HashMap::new();
for dir in &app_dirs() { for dir in &app_dirs() {
let Ok(entries) = fs::read_dir(dir) else { continue }; 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") { if path.extension().and_then(|e| e.to_str()) != Some("desktop") {
continue; continue;
} }
let id = entry.file_name().to_string_lossy().into_owned();
if let Some(app) = parse_desktop(&path) { 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 { fn pick_terminal() -> String {
if let Ok(t) = env::var("TERMINAL") { if let Ok(t) = env::var("TERMINAL") {
if !t.is_empty() { if !t.is_empty() {
@ -177,83 +234,7 @@ fn pick_terminal() -> String {
"xterm".to_string() "xterm".to_string()
} }
fn main() { fn do_launch(action: &str) {
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("");
if let Some(cmd) = action.strip_prefix("app::") { if let Some(cmd) = action.strip_prefix("app::") {
let _ = Command::new("bash") let _ = Command::new("bash")
.args(["-c", cmd]) .args(["-c", cmd])
@ -271,3 +252,265 @@ fn main() {
.spawn(); .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::<u32>() {
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: &gtk4::ListBoxRow, key: &str) -> String {
unsafe {
row.data::<String>(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<gtk4::ListBoxRow> = 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);
}