Initial Wayfinder release
This commit is contained in:
commit
e8513b943a
16 files changed
+8234
No files matched your search
Generated
+671
@@ -0,0 +1,671 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# 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.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cairo-rs"
|
||||||
|
version = "0.21.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b01fe135c0bd16afe262b6dea349bd5ea30e6de50708cec639aae7c5c14cc7e4"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cairo-sys-rs",
|
||||||
|
"glib",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cairo-sys-rs"
|
||||||
|
version = "0.21.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06c28280c6b12055b5e39e4554271ae4e6630b27c0da9148c4cf6485fc6d245c"
|
||||||
|
dependencies = [
|
||||||
|
"glib-sys",
|
||||||
|
"libc",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-expr"
|
||||||
|
version = "0.20.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ba9e9ec16c447027685b1f897b720e18e9a8afd00bd7332c483537e38086c9f"
|
||||||
|
dependencies = [
|
||||||
|
"smallvec",
|
||||||
|
"target-lexicon",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
|
||||||
|
|
||||||
|
[[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.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-executor"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-io"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-macro"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.9",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-macro",
|
||||||
|
"futures-task",
|
||||||
|
"pin-project-lite",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gdk-pixbuf"
|
||||||
|
version = "0.21.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "debb0d39e3cdd84626edfd54d6e4a6ba2da9a0ef2e796e691c4e9f8646fda00c"
|
||||||
|
dependencies = [
|
||||||
|
"gdk-pixbuf-sys",
|
||||||
|
"gio",
|
||||||
|
"glib",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gdk-pixbuf-sys"
|
||||||
|
version = "0.21.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd95ad50b9a3d2551e25dd4f6892aff0b772fe5372d84514e9d0583af60a0ce7"
|
||||||
|
dependencies = [
|
||||||
|
"gio-sys",
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"libc",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gdk4"
|
||||||
|
version = "0.10.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "756564212bbe4a4ce05d88ffbd2582581ac6003832d0d32822d0825cca84bfbf"
|
||||||
|
dependencies = [
|
||||||
|
"cairo-rs",
|
||||||
|
"gdk-pixbuf",
|
||||||
|
"gdk4-sys",
|
||||||
|
"gio",
|
||||||
|
"glib",
|
||||||
|
"libc",
|
||||||
|
"pango",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gdk4-sys"
|
||||||
|
version = "0.10.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6d4e5b3ccf591826a4adcc83f5f57b4e59d1925cb4bf620b0d645f79498b034"
|
||||||
|
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.21.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c5ff48bf600c68b476e61dc6b7c762f2f4eb91deef66583ba8bb815c30b5811a"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"futures-util",
|
||||||
|
"gio-sys",
|
||||||
|
"glib",
|
||||||
|
"libc",
|
||||||
|
"pin-project-lite",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gio-sys"
|
||||||
|
version = "0.21.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0071fe88dba8e40086c8ff9bbb62622999f49628344b1d1bf490a48a29d80f22"
|
||||||
|
dependencies = [
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"libc",
|
||||||
|
"system-deps",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glib"
|
||||||
|
version = "0.21.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "16de123c2e6c90ce3b573b7330de19be649080ec612033d397d72da265f1bd8b"
|
||||||
|
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.21.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf59b675301228a696fe01c3073974643365080a76cc3ed5bc2cbc466ad87f17"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro-crate",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glib-sys"
|
||||||
|
version = "0.21.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2d95e1a3a19ae464a7286e14af9a90683c64d70c02532d88d87ce95056af3e6c"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gobject-sys"
|
||||||
|
version = "0.21.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2dca35da0d19a18f4575f3cb99fe1c9e029a2941af5662f326f738a21edaf294"
|
||||||
|
dependencies = [
|
||||||
|
"glib-sys",
|
||||||
|
"libc",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "graphene-rs"
|
||||||
|
version = "0.21.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2730030ac9db663fd8bfe1e7093742c1cafb92db9c315c9417c29032341fe2f9"
|
||||||
|
dependencies = [
|
||||||
|
"glib",
|
||||||
|
"graphene-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "graphene-sys"
|
||||||
|
version = "0.21.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "915e32091ea9ad241e4b044af62b7351c2d68aeb24f489a0d7f37a0fc484fd93"
|
||||||
|
dependencies = [
|
||||||
|
"glib-sys",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gsk4"
|
||||||
|
version = "0.10.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e755de9d8c5896c5beaa028b89e1969d067f1b9bf1511384ede971f5983aa153"
|
||||||
|
dependencies = [
|
||||||
|
"cairo-rs",
|
||||||
|
"gdk4",
|
||||||
|
"glib",
|
||||||
|
"graphene-rs",
|
||||||
|
"gsk4-sys",
|
||||||
|
"libc",
|
||||||
|
"pango",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gsk4-sys"
|
||||||
|
version = "0.10.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7ce91472391146f482065f1041876d8f869057b195b95399414caa163d72f4f7"
|
||||||
|
dependencies = [
|
||||||
|
"cairo-sys-rs",
|
||||||
|
"gdk4-sys",
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"graphene-sys",
|
||||||
|
"libc",
|
||||||
|
"pango-sys",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gtk4"
|
||||||
|
version = "0.10.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "acb21d53cfc6f7bfaf43549731c43b67ca47d87348d81c8cfc4dcdd44828e1a4"
|
||||||
|
dependencies = [
|
||||||
|
"cairo-rs",
|
||||||
|
"field-offset",
|
||||||
|
"futures-channel",
|
||||||
|
"gdk-pixbuf",
|
||||||
|
"gdk4",
|
||||||
|
"gio",
|
||||||
|
"glib",
|
||||||
|
"graphene-rs",
|
||||||
|
"gsk4",
|
||||||
|
"gtk4-macros",
|
||||||
|
"gtk4-sys",
|
||||||
|
"libc",
|
||||||
|
"pango",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gtk4-macros"
|
||||||
|
version = "0.10.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ccfb5a14a3d941244815d5f8101fa12d4577b59cc47245778d8d907b0003e42"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-crate",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gtk4-sys"
|
||||||
|
version = "0.10.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "842577fe5a1ee15d166cd3afe804ce0cab6173bc789ca32e21308834f20088dd"
|
||||||
|
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.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.190"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||||
|
|
||||||
|
[[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.21.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52d1d85e2078077a065bb7fc072783d5bcd4e51b379f22d67107d0a16937eb69"
|
||||||
|
dependencies = [
|
||||||
|
"gio",
|
||||||
|
"glib",
|
||||||
|
"libc",
|
||||||
|
"pango-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pango-sys"
|
||||||
|
version = "0.21.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b4f06627d36ed5ff303d2df65211fc2e52ba5b17bf18dd80ff3d9628d6e06cfd"
|
||||||
|
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.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
|
||||||
|
|
||||||
|
[[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.107"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.47"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||||
|
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.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.9",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c"
|
||||||
|
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.16.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.119"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "3.0.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
|
||||||
|
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.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "1.1.8+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde_core",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_parser",
|
||||||
|
"toml_writer",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "1.1.2+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.25.17+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_parser",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_parser"
|
||||||
|
version = "1.1.5+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c"
|
||||||
|
dependencies = [
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_writer"
|
||||||
|
version = "1.1.3+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version-compare"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wayfinder"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"gtk4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "wayfinder"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
rust-version = "1.85"
|
||||||
|
description = "A compact GTK4 file manager for Wayland desktops"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gtk = { package = "gtk4", version = "0.10" }
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# Wayfinder
|
||||||
|
|
||||||
|
Wayfinder is a compact graphical file manager for Linux desktops running Wayland. It is written in Rust and uses GTK 4. The main view is a directory listing with a configurable places sidebar; selecting a supported file opens an inline preview pane.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
- Browse directories by entering a path, using the places sidebar, or opening a folder from the file list.
|
||||||
|
- Filter the current directory, sort folders before files, and toggle hidden files with `Ctrl+H`.
|
||||||
|
- See file sizes and relative modification times in the list.
|
||||||
|
- Preview supported text and source files, with syntax colors enabled by default, and preview images in the side pane. Text previews are capped at 64 KiB by default.
|
||||||
|
- Open text files in Neovim inside Foot, images in imv, executable files in Foot, and other files with their default desktop application.
|
||||||
|
- Use keyboard shortcuts: `Ctrl+L` focuses the location field, `Backspace` goes to the parent directory, `F5` reloads the current directory, and `Esc` exits preview.
|
||||||
|
- Adjust window, pane, layout, color, place, and preview settings through nested INI configuration files.
|
||||||
|
|
||||||
|
Wayfinder is focused on browsing and previewing. It does not currently provide file copy, move, delete, rename, or mount operations.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
Build requirements:
|
||||||
|
|
||||||
|
- Rust and Cargo (Rust 1.85 or newer)
|
||||||
|
- GTK 4 development files and `pkg-config`
|
||||||
|
|
||||||
|
The application uses GTK's Wayland support when launched in a Wayland session. Image preview uses `gdk-pixbuf-thumbnailer` in a Bubblewrap sandbox. JPEG preview also has a Python 3 and Pillow fallback for systems where GdkPixbuf lacks a JPEG loader. To open files from the list, Wayfinder can use Foot and Neovim for text, imv for images, and the system's default application for other file types.
|
||||||
|
|
||||||
|
## Build and run
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build --release
|
||||||
|
./target/release/wayfinder
|
||||||
|
```
|
||||||
|
|
||||||
|
A Gentoo ebuild is provided under `packaging/gentoo/gui-apps/wayfinder/`.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Wayfinder reads `~/.config/wayfinder/config.ini`. Copy the example files from the source tree:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir -p ~/.config/wayfinder
|
||||||
|
cp config.ini colors.ini ~/.config/wayfinder/
|
||||||
|
```
|
||||||
|
|
||||||
|
`config.ini` contains window and pane sizes, spacing, typography, preview limits, and places. It includes `colors.ini`, where all interface and syntax colors are defined. Set `code_preview = false` in the `[General]` section to disable syntax coloring.
|
||||||
|
|
||||||
|
Configuration files can include other INI files recursively. Add comma-separated file names under `[Includes]` in `config.ini` (for example, `files = colors.ini, work.ini`). Relative include paths are resolved from the including file, included values load first, and the including file overrides matching values. Places are defined in `[Places]` as `Name = path`; `~` and `~/...` paths are supported.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Wayfinder is distributed under the MIT License. See `LICENSE` in the source repository.
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
; Every application color is defined here and can be overridden in config.ini
|
||||||
|
; or in any nested INI file listed in [Includes]. Values must be #RGB, #RGBA,
|
||||||
|
; #RRGGBB, or #RRGGBBAA hexadecimal colors.
|
||||||
|
[Colors]
|
||||||
|
background = #151515
|
||||||
|
panel = #181818
|
||||||
|
surface = #202020
|
||||||
|
hover = #282828
|
||||||
|
selected = #303030
|
||||||
|
border = #2F2F2F
|
||||||
|
focus_border = #454545
|
||||||
|
text = #D0D0D0
|
||||||
|
muted = #858585
|
||||||
|
dim = #505050
|
||||||
|
emphasis = #F5F5F5
|
||||||
|
accent = #6C99BB
|
||||||
|
accent_secondary = #547C99
|
||||||
|
directory = #6C99BB
|
||||||
|
success = #7B9246
|
||||||
|
warning = #D3A04D
|
||||||
|
danger = #A53C23
|
||||||
|
teal = #5EA8A2
|
||||||
|
; Syntax colors follow the active Aether Neovim palette.
|
||||||
|
code_keyword = #9F4E85
|
||||||
|
code_string = #7B9246
|
||||||
|
code_number = #D3A04D
|
||||||
|
code_comment = #505050
|
||||||
|
code_function = #6C99BB
|
||||||
|
code_type = #5EA8A2
|
||||||
|
code_constant = #A53C23
|
||||||
|
code_operator = #547C99
|
||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
; Wayfinder master configuration.
|
||||||
|
; Values in this file override included files. Include files can include other .ini files.
|
||||||
|
; Place these files in ~/.config/wayfinder/.
|
||||||
|
|
||||||
|
[Includes]
|
||||||
|
files = colors.ini
|
||||||
|
|
||||||
|
[General]
|
||||||
|
; Window and pane sizes, in pixels.
|
||||||
|
window_width = 1080
|
||||||
|
window_height = 700
|
||||||
|
sidebar_width = 136
|
||||||
|
preview_width = 320
|
||||||
|
compact_columns_width = 520
|
||||||
|
|
||||||
|
; Preview behavior.
|
||||||
|
text_preview_limit_kib = 64
|
||||||
|
image_preview_size = 512
|
||||||
|
; Colorize source and configuration files in the preview pane.
|
||||||
|
code_preview = true
|
||||||
|
|
||||||
|
; Toolbar allocation: Filter receives this percentage of available entry width.
|
||||||
|
filter_width_ratio = 30
|
||||||
|
filter_width_min = 84
|
||||||
|
filter_width_max = 180
|
||||||
|
|
||||||
|
; Typography.
|
||||||
|
font_family = Maple Mono, "JetBrainsMono Nerd Font", monospace
|
||||||
|
font_size = 10
|
||||||
|
|
||||||
|
; Spacing and component dimensions. Numeric values are pixels unless noted as font sizes.
|
||||||
|
toolbar_pad_x = 10
|
||||||
|
toolbar_pad_y = 8
|
||||||
|
toolbar_gap = 6
|
||||||
|
control_pad_x = 9
|
||||||
|
control_pad_y = 7
|
||||||
|
control_min_height = 18
|
||||||
|
button_size = 34
|
||||||
|
icon_size = 16
|
||||||
|
corner_radius = 2
|
||||||
|
sidebar_pad_x = 6
|
||||||
|
sidebar_pad_y = 8
|
||||||
|
place_pad_x = 9
|
||||||
|
place_pad_y = 7
|
||||||
|
place_min_height = 20
|
||||||
|
heading_pad_x = 8
|
||||||
|
heading_pad_y = 9
|
||||||
|
preview_pad = 12
|
||||||
|
preview_spacing = 10
|
||||||
|
preview_heading_size = 11
|
||||||
|
preview_title_size = 10
|
||||||
|
preview_details_size = 9
|
||||||
|
preview_title_pad = 2
|
||||||
|
preview_message_pad = 12
|
||||||
|
preview_text_pad = 10
|
||||||
|
top_border_width = 2
|
||||||
|
border_width = 1
|
||||||
|
row_height = 36
|
||||||
|
row_padding_x = 8
|
||||||
|
row_padding_y = 5
|
||||||
|
size_column_width = 72
|
||||||
|
modified_column_width = 126
|
||||||
|
metadata_value_height = 24
|
||||||
|
place_gap = 2
|
||||||
|
status_margin_x = 12
|
||||||
|
status_margin_y = 6
|
||||||
|
metadata_font_size = 10
|
||||||
|
metadata_pad_top = 3
|
||||||
|
metadata_pad_bottom = 5
|
||||||
|
scrollbar_width = 6
|
||||||
|
scrollbar_thumb_width = 4
|
||||||
|
scrollbar_thumb_radius = 0
|
||||||
|
column_gap = 8
|
||||||
|
|
||||||
|
; Each key is the visible shortcut name; values may be absolute paths or use ~/.
|
||||||
|
[Places]
|
||||||
|
Home = ~
|
||||||
|
Documents = ~/Documents
|
||||||
|
Downloads = ~/Downloads
|
||||||
|
Pictures = ~/Pictures
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Name=Wayfinder
|
||||||
|
Comment=Browse files and preview text and images
|
||||||
|
Exec=wayfinder
|
||||||
|
Icon=system-file-manager
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Categories=System;FileTools;FileManager;
|
||||||
|
StartupNotify=true
|
||||||
|
DBusActivatable=false
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# Copyright 2026 Wiktor A. Falkowski
|
||||||
|
# Distributed under the terms of the MIT License
|
||||||
|
|
||||||
|
EAPI=8
|
||||||
|
|
||||||
|
CRATES="
|
||||||
|
autocfg@1.5.1
|
||||||
|
bitflags@2.13.2
|
||||||
|
cairo-rs@0.21.5
|
||||||
|
cairo-sys-rs@0.21.5
|
||||||
|
cfg-expr@0.20.10
|
||||||
|
equivalent@1.0.3
|
||||||
|
field-offset@0.3.6
|
||||||
|
futures-channel@0.3.34
|
||||||
|
futures-core@0.3.34
|
||||||
|
futures-executor@0.3.34
|
||||||
|
futures-io@0.3.34
|
||||||
|
futures-macro@0.3.34
|
||||||
|
futures-task@0.3.34
|
||||||
|
futures-util@0.3.34
|
||||||
|
gdk-pixbuf-sys@0.21.5
|
||||||
|
gdk-pixbuf@0.21.5
|
||||||
|
gdk4-sys@0.10.3
|
||||||
|
gdk4@0.10.3
|
||||||
|
gio-sys@0.21.5
|
||||||
|
gio@0.21.5
|
||||||
|
glib-macros@0.21.5
|
||||||
|
glib-sys@0.21.5
|
||||||
|
glib@0.21.5
|
||||||
|
gobject-sys@0.21.5
|
||||||
|
graphene-rs@0.21.5
|
||||||
|
graphene-sys@0.21.5
|
||||||
|
gsk4-sys@0.10.3
|
||||||
|
gsk4@0.10.3
|
||||||
|
gtk4-macros@0.10.3
|
||||||
|
gtk4-sys@0.10.3
|
||||||
|
gtk4@0.10.3
|
||||||
|
hashbrown@0.17.1
|
||||||
|
heck@0.5.0
|
||||||
|
indexmap@2.14.2
|
||||||
|
libc@0.2.190
|
||||||
|
memchr@2.8.3
|
||||||
|
memoffset@0.9.1
|
||||||
|
pango-sys@0.21.5
|
||||||
|
pango@0.21.5
|
||||||
|
pin-project-lite@0.2.17
|
||||||
|
pkg-config@0.3.34
|
||||||
|
proc-macro-crate@3.5.0
|
||||||
|
proc-macro2@1.0.107
|
||||||
|
quote@1.0.47
|
||||||
|
rustc_version@0.4.1
|
||||||
|
semver@1.0.28
|
||||||
|
serde_core@1.0.229
|
||||||
|
serde_derive@1.0.229
|
||||||
|
serde_spanned@1.1.2
|
||||||
|
slab@0.4.12
|
||||||
|
smallvec@1.16.3
|
||||||
|
syn@2.0.119
|
||||||
|
syn@3.0.9
|
||||||
|
system-deps@7.0.8
|
||||||
|
target-lexicon@0.13.5
|
||||||
|
toml@1.1.8+spec-1.1.0
|
||||||
|
toml_datetime@1.1.2+spec-1.1.0
|
||||||
|
toml_edit@0.25.17+spec-1.1.0
|
||||||
|
toml_parser@1.1.5+spec-1.1.0
|
||||||
|
toml_writer@1.1.3+spec-1.1.0
|
||||||
|
unicode-ident@1.0.27
|
||||||
|
version-compare@0.2.1
|
||||||
|
windows-link@0.2.1
|
||||||
|
windows-sys@0.61.2
|
||||||
|
winnow@1.0.4
|
||||||
|
"
|
||||||
|
|
||||||
|
RUST_MIN_VER="1.85.0"
|
||||||
|
inherit cargo
|
||||||
|
|
||||||
|
DESCRIPTION="Compact GTK4 file manager for Wayland desktops"
|
||||||
|
HOMEPAGE="https://git.anchorhost.eu/wfalkowski/wayfinder"
|
||||||
|
SRC_URI="https://git.anchorhost.eu/wfalkowski/wayfinder/archive/v${PV}.tar.gz -> ${P}.tar.gz ${CARGO_CRATE_URIS}"
|
||||||
|
S="${WORKDIR}/wayfinder"
|
||||||
|
|
||||||
|
LICENSE="MIT Apache-2.0 Apache-2.0-with-LLVM-exceptions Unicode-3.0"
|
||||||
|
SLOT="0"
|
||||||
|
KEYWORDS="~amd64"
|
||||||
|
|
||||||
|
DEPEND="gui-libs/gtk:4[wayland]"
|
||||||
|
RDEPEND="
|
||||||
|
${DEPEND}
|
||||||
|
sys-apps/bubblewrap
|
||||||
|
media-libs/gdk-pixbuf
|
||||||
|
"
|
||||||
|
BDEPEND="virtual/pkgconfig"
|
||||||
|
|
||||||
|
src_configure() {
|
||||||
|
cargo_src_configure --locked --bin wayfinder
|
||||||
|
}
|
||||||
|
|
||||||
|
src_install() {
|
||||||
|
cargo_src_install
|
||||||
|
insinto /usr/share/applications
|
||||||
|
doins data/dev.wfalkowski.Wayfinder.desktop
|
||||||
|
dodoc README.md LICENSE
|
||||||
|
insinto /usr/share/wayfinder
|
||||||
|
doins config.ini colors.ini
|
||||||
|
}
|
||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
# Copyright 2026 Wiktor A. Falkowski
|
||||||
|
# Distributed under the terms of the MIT License
|
||||||
|
|
||||||
|
EAPI=8
|
||||||
|
|
||||||
|
CRATES="
|
||||||
|
autocfg@1.5.1
|
||||||
|
bitflags@2.13.2
|
||||||
|
cairo-rs@0.21.5
|
||||||
|
cairo-sys-rs@0.21.5
|
||||||
|
cfg-expr@0.20.10
|
||||||
|
equivalent@1.0.3
|
||||||
|
field-offset@0.3.6
|
||||||
|
futures-channel@0.3.34
|
||||||
|
futures-core@0.3.34
|
||||||
|
futures-executor@0.3.34
|
||||||
|
futures-io@0.3.34
|
||||||
|
futures-macro@0.3.34
|
||||||
|
futures-task@0.3.34
|
||||||
|
futures-util@0.3.34
|
||||||
|
gdk-pixbuf@0.21.5
|
||||||
|
gdk-pixbuf-sys@0.21.5
|
||||||
|
gdk4@0.10.3
|
||||||
|
gdk4-sys@0.10.3
|
||||||
|
gio@0.21.5
|
||||||
|
gio-sys@0.21.5
|
||||||
|
glib@0.21.5
|
||||||
|
glib-macros@0.21.5
|
||||||
|
glib-sys@0.21.5
|
||||||
|
gobject-sys@0.21.5
|
||||||
|
graphene-rs@0.21.5
|
||||||
|
graphene-sys@0.21.5
|
||||||
|
gsk4@0.10.3
|
||||||
|
gsk4-sys@0.10.3
|
||||||
|
gtk4@0.10.3
|
||||||
|
gtk4-macros@0.10.3
|
||||||
|
gtk4-sys@0.10.3
|
||||||
|
hashbrown@0.17.1
|
||||||
|
heck@0.5.0
|
||||||
|
indexmap@2.14.2
|
||||||
|
libc@0.2.190
|
||||||
|
memchr@2.8.3
|
||||||
|
memoffset@0.9.1
|
||||||
|
pango@0.21.5
|
||||||
|
pango-sys@0.21.5
|
||||||
|
pin-project-lite@0.2.17
|
||||||
|
pkg-config@0.3.34
|
||||||
|
proc-macro-crate@3.5.0
|
||||||
|
proc-macro2@1.0.107
|
||||||
|
quote@1.0.47
|
||||||
|
rustc-version@0.4.1
|
||||||
|
semver@1.0.28
|
||||||
|
serde-core@1.0.229
|
||||||
|
serde-derive@1.0.229
|
||||||
|
serde-spanned@1.1.2
|
||||||
|
slab@0.4.12
|
||||||
|
smallvec@1.16.3
|
||||||
|
syn@2.0.119
|
||||||
|
syn@3.0.9
|
||||||
|
system-deps@7.0.8
|
||||||
|
target-lexicon@0.13.5
|
||||||
|
toml@1.1.8+spec-1.1.0
|
||||||
|
toml-datetime@1.1.2+spec-1.1.0
|
||||||
|
toml-edit@0.25.17+spec-1.1.0
|
||||||
|
toml-parser@1.1.5+spec-1.1.0
|
||||||
|
toml-writer@1.1.3+spec-1.1.0
|
||||||
|
unicode-ident@1.0.27
|
||||||
|
version-compare@0.2.1
|
||||||
|
windows-link@0.2.1
|
||||||
|
windows-sys@0.61.2
|
||||||
|
winnow@1.0.4
|
||||||
|
"
|
||||||
|
|
||||||
|
RUST_MIN_VER="1.85.0"
|
||||||
|
inherit cargo
|
||||||
|
|
||||||
|
DESCRIPTION="Compact GTK4 file manager for Wayland desktops"
|
||||||
|
HOMEPAGE="https://git.anchorhost.eu/wfalkowski/wayfinder"
|
||||||
|
SRC_URI="https://git.anchorhost.eu/wfalkowski/wayfinder/archive/v${PV}.tar.gz -> ${P}.tar.gz ${CARGO_CRATE_URIS}"
|
||||||
|
S="${WORKDIR}/wayfinder"
|
||||||
|
|
||||||
|
LICENSE="MIT Apache-2.0 Apache-2.0-with-LLVM-exceptions Unicode-3.0"
|
||||||
|
SLOT="0"
|
||||||
|
KEYWORDS="~amd64"
|
||||||
|
|
||||||
|
DEPEND="gui-libs/gtk:4[wayland]"
|
||||||
|
RDEPEND="
|
||||||
|
${DEPEND}
|
||||||
|
sys-apps/bubblewrap
|
||||||
|
media-libs/gdk-pixbuf
|
||||||
|
"
|
||||||
|
BDEPEND="virtual/pkgconfig"
|
||||||
|
|
||||||
|
src_configure() {
|
||||||
|
cargo_src_configure --locked --bin wayfinder
|
||||||
|
}
|
||||||
|
|
||||||
|
src_install() {
|
||||||
|
cargo_src_install
|
||||||
|
insinto /usr/share/applications
|
||||||
|
doins data/dev.wfalkowski.Wayfinder.desktop
|
||||||
|
dodoc README.md LICENSE config.ini colors.ini
|
||||||
|
}
|
||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
# Copyright 2026 Wiktor A. Falkowski
|
||||||
|
# Distributed under the terms of the MIT License
|
||||||
|
|
||||||
|
EAPI=8
|
||||||
|
|
||||||
|
CRATES="
|
||||||
|
autocfg@1.5.1
|
||||||
|
bitflags@2.13.2
|
||||||
|
cairo-rs@0.21.5
|
||||||
|
cairo-sys-rs@0.21.5
|
||||||
|
cfg-expr@0.20.10
|
||||||
|
equivalent@1.0.3
|
||||||
|
field-offset@0.3.6
|
||||||
|
futures-channel@0.3.34
|
||||||
|
futures-core@0.3.34
|
||||||
|
futures-executor@0.3.34
|
||||||
|
futures-io@0.3.34
|
||||||
|
futures-macro@0.3.34
|
||||||
|
futures-task@0.3.34
|
||||||
|
futures-util@0.3.34
|
||||||
|
gdk-pixbuf@0.21.5
|
||||||
|
gdk-pixbuf-sys@0.21.5
|
||||||
|
gdk4@0.10.3
|
||||||
|
gdk4-sys@0.10.3
|
||||||
|
gio@0.21.5
|
||||||
|
gio-sys@0.21.5
|
||||||
|
glib@0.21.5
|
||||||
|
glib-macros@0.21.5
|
||||||
|
glib-sys@0.21.5
|
||||||
|
gobject-sys@0.21.5
|
||||||
|
graphene-rs@0.21.5
|
||||||
|
graphene-sys@0.21.5
|
||||||
|
gsk4@0.10.3
|
||||||
|
gsk4-sys@0.10.3
|
||||||
|
gtk4@0.10.3
|
||||||
|
gtk4-macros@0.10.3
|
||||||
|
gtk4-sys@0.10.3
|
||||||
|
hashbrown@0.17.1
|
||||||
|
heck@0.5.0
|
||||||
|
indexmap@2.14.2
|
||||||
|
libc@0.2.190
|
||||||
|
memchr@2.8.3
|
||||||
|
memoffset@0.9.1
|
||||||
|
pango@0.21.5
|
||||||
|
pango-sys@0.21.5
|
||||||
|
pin-project-lite@0.2.17
|
||||||
|
pkg-config@0.3.34
|
||||||
|
proc-macro-crate@3.5.0
|
||||||
|
proc-macro2@1.0.107
|
||||||
|
quote@1.0.47
|
||||||
|
rustc-version@0.4.1
|
||||||
|
semver@1.0.28
|
||||||
|
serde-core@1.0.229
|
||||||
|
serde-derive@1.0.229
|
||||||
|
serde-spanned@1.1.2
|
||||||
|
slab@0.4.12
|
||||||
|
smallvec@1.16.3
|
||||||
|
syn@2.0.119
|
||||||
|
syn@3.0.9
|
||||||
|
system-deps@7.0.8
|
||||||
|
target-lexicon@0.13.5
|
||||||
|
toml@1.1.8+spec-1.1.0
|
||||||
|
toml-datetime@1.1.2+spec-1.1.0
|
||||||
|
toml-edit@0.25.17+spec-1.1.0
|
||||||
|
toml-parser@1.1.5+spec-1.1.0
|
||||||
|
toml-writer@1.1.3+spec-1.1.0
|
||||||
|
unicode-ident@1.0.27
|
||||||
|
version-compare@0.2.1
|
||||||
|
windows-link@0.2.1
|
||||||
|
windows-sys@0.61.2
|
||||||
|
winnow@1.0.4
|
||||||
|
"
|
||||||
|
|
||||||
|
RUST_MIN_VER="1.85.0"
|
||||||
|
inherit cargo
|
||||||
|
|
||||||
|
DESCRIPTION="Compact GTK4 file manager for Wayland desktops"
|
||||||
|
HOMEPAGE="https://git.anchorhost.eu/wfalkowski/wayfinder"
|
||||||
|
SRC_URI="https://git.anchorhost.eu/wfalkowski/wayfinder/archive/v${PV}.tar.gz -> ${P}.tar.gz ${CARGO_CRATE_URIS}"
|
||||||
|
S="${WORKDIR}/wayfinder"
|
||||||
|
|
||||||
|
LICENSE="MIT Apache-2.0 Apache-2.0-with-LLVM-exceptions Unicode-3.0"
|
||||||
|
SLOT="0"
|
||||||
|
KEYWORDS="~amd64"
|
||||||
|
|
||||||
|
DEPEND="gui-libs/gtk:4[wayland]"
|
||||||
|
RDEPEND="
|
||||||
|
${DEPEND}
|
||||||
|
sys-apps/bubblewrap
|
||||||
|
media-libs/gdk-pixbuf
|
||||||
|
"
|
||||||
|
BDEPEND="virtual/pkgconfig"
|
||||||
|
|
||||||
|
src_configure() {
|
||||||
|
cargo_src_configure --locked --bin wayfinder
|
||||||
|
}
|
||||||
|
|
||||||
|
src_install() {
|
||||||
|
cargo_src_install
|
||||||
|
insinto /usr/share/applications
|
||||||
|
doins data/dev.wfalkowski.Wayfinder.desktop
|
||||||
|
dodoc README.md LICENSE
|
||||||
|
insinto /usr/share/wayfinder
|
||||||
|
doins config.ini colors.ini
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# Copyright 2026 Wiktor A. Falkowski
|
||||||
|
# Distributed under the terms of the MIT License
|
||||||
|
|
||||||
|
EAPI=8
|
||||||
|
|
||||||
|
CRATES="
|
||||||
|
autocfg@1.5.1
|
||||||
|
bitflags@2.13.2
|
||||||
|
cairo-rs@0.21.5
|
||||||
|
cairo-sys-rs@0.21.5
|
||||||
|
cfg-expr@0.20.10
|
||||||
|
equivalent@1.0.3
|
||||||
|
field-offset@0.3.6
|
||||||
|
futures-channel@0.3.34
|
||||||
|
futures-core@0.3.34
|
||||||
|
futures-executor@0.3.34
|
||||||
|
futures-io@0.3.34
|
||||||
|
futures-macro@0.3.34
|
||||||
|
futures-task@0.3.34
|
||||||
|
futures-util@0.3.34
|
||||||
|
gdk-pixbuf@0.21.5
|
||||||
|
gdk-pixbuf-sys@0.21.5
|
||||||
|
gdk4@0.10.3
|
||||||
|
gdk4-sys@0.10.3
|
||||||
|
gio@0.21.5
|
||||||
|
gio-sys@0.21.5
|
||||||
|
glib@0.21.5
|
||||||
|
glib-macros@0.21.5
|
||||||
|
glib-sys@0.21.5
|
||||||
|
gobject-sys@0.21.5
|
||||||
|
graphene-rs@0.21.5
|
||||||
|
graphene-sys@0.21.5
|
||||||
|
gsk4@0.10.3
|
||||||
|
gsk4-sys@0.10.3
|
||||||
|
gtk4@0.10.3
|
||||||
|
gtk4-macros@0.10.3
|
||||||
|
gtk4-sys@0.10.3
|
||||||
|
hashbrown@0.17.1
|
||||||
|
heck@0.5.0
|
||||||
|
indexmap@2.14.2
|
||||||
|
libc@0.2.190
|
||||||
|
memchr@2.8.3
|
||||||
|
memoffset@0.9.1
|
||||||
|
pango@0.21.5
|
||||||
|
pango-sys@0.21.5
|
||||||
|
pin-project-lite@0.2.17
|
||||||
|
pkg-config@0.3.34
|
||||||
|
proc-macro-crate@3.5.0
|
||||||
|
proc-macro2@1.0.107
|
||||||
|
quote@1.0.47
|
||||||
|
rustc-version@0.4.1
|
||||||
|
semver@1.0.28
|
||||||
|
serde-core@1.0.229
|
||||||
|
serde-derive@1.0.229
|
||||||
|
serde-spanned@1.1.2
|
||||||
|
slab@0.4.12
|
||||||
|
smallvec@1.16.3
|
||||||
|
syn@2.0.119
|
||||||
|
syn@3.0.9
|
||||||
|
system-deps@7.0.8
|
||||||
|
target-lexicon@0.13.5
|
||||||
|
toml@1.1.8+spec-1.1.0
|
||||||
|
toml-datetime@1.1.2+spec-1.1.0
|
||||||
|
toml-edit@0.25.17+spec-1.1.0
|
||||||
|
toml-parser@1.1.5+spec-1.1.0
|
||||||
|
toml-writer@1.1.3+spec-1.1.0
|
||||||
|
unicode-ident@1.0.27
|
||||||
|
version-compare@0.2.1
|
||||||
|
windows-link@0.2.1
|
||||||
|
windows-sys@0.61.2
|
||||||
|
winnow@1.0.4
|
||||||
|
"
|
||||||
|
|
||||||
|
RUST_MIN_VER="1.85.0"
|
||||||
|
inherit cargo
|
||||||
|
|
||||||
|
DESCRIPTION="Compact GTK4 file manager for Wayland desktops"
|
||||||
|
HOMEPAGE="https://git.anchorhost.eu/wfalkowski/wayfinder"
|
||||||
|
SRC_URI="https://git.anchorhost.eu/wfalkowski/wayfinder/archive/v${PV}.tar.gz -> ${P}.tar.gz ${CARGO_CRATE_URIS}"
|
||||||
|
S="${WORKDIR}/wayfinder"
|
||||||
|
|
||||||
|
LICENSE="MIT Apache-2.0 Apache-2.0-with-LLVM-exceptions Unicode-3.0"
|
||||||
|
SLOT="0"
|
||||||
|
KEYWORDS="~amd64"
|
||||||
|
|
||||||
|
DEPEND="gui-libs/gtk:4[wayland]"
|
||||||
|
RDEPEND="
|
||||||
|
${DEPEND}
|
||||||
|
app-misc/ bubblewrap
|
||||||
|
media-libs/gdk-pixbuf
|
||||||
|
"
|
||||||
|
BDEPEND="virtual/pkgconfig"
|
||||||
|
|
||||||
|
src_configure() {
|
||||||
|
cargo_src_configure --locked --bin wayfinder
|
||||||
|
}
|
||||||
|
|
||||||
|
src_install() {
|
||||||
|
cargo_src_install
|
||||||
|
domenu data/dev.wfalkowski.Wayfinder.desktop
|
||||||
|
dodoc README.md LICENSE config.ini colors.ini
|
||||||
|
}
|
||||||
+1764
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,498 @@
|
|||||||
|
use gtk::gio;
|
||||||
|
use gtk::glib;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::fs;
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Command;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Entry {
|
||||||
|
path: PathBuf,
|
||||||
|
name: String,
|
||||||
|
directory: bool,
|
||||||
|
size: u64,
|
||||||
|
modified: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Browser {
|
||||||
|
location: Rc<RefCell<PathBuf>>,
|
||||||
|
show_hidden: Rc<RefCell<bool>>,
|
||||||
|
list: gtk::ListBox,
|
||||||
|
location_entry: gtk::Entry,
|
||||||
|
status: gtk::Label,
|
||||||
|
filter: gtk::SearchEntry,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let app = gtk::Application::builder()
|
||||||
|
.application_id("dev.wfalkowski.Wayfinder")
|
||||||
|
.flags(gio::ApplicationFlags::NON_UNIQUE)
|
||||||
|
.build();
|
||||||
|
app.connect_activate(build_ui);
|
||||||
|
app.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_ui(app: >k::Application) {
|
||||||
|
install_style();
|
||||||
|
let home = std::env::var_os("HOME")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|| PathBuf::from("/"));
|
||||||
|
let window = gtk::ApplicationWindow::builder()
|
||||||
|
.application(app)
|
||||||
|
.title("Wayfinder")
|
||||||
|
.default_width(1080)
|
||||||
|
.default_height(700)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let root = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||||
|
root.add_css_class("root");
|
||||||
|
let toolbar = gtk::Box::new(gtk::Orientation::Horizontal, 6);
|
||||||
|
toolbar.add_css_class("toolbar");
|
||||||
|
|
||||||
|
let back = tool_button("", "Go to parent folder");
|
||||||
|
let back_icon = gtk::Image::from_icon_name("go-previous");
|
||||||
|
back_icon.set_pixel_size(16);
|
||||||
|
back_icon.set_halign(gtk::Align::Center);
|
||||||
|
back_icon.set_valign(gtk::Align::Center);
|
||||||
|
back.set_child(Some(&back_icon));
|
||||||
|
back.add_css_class("back-button");
|
||||||
|
let refresh = tool_button("↻", "Reload this folder");
|
||||||
|
let location_entry = gtk::Entry::builder()
|
||||||
|
.hexpand(true)
|
||||||
|
.placeholder_text("Enter a folder path")
|
||||||
|
.build();
|
||||||
|
location_entry.add_css_class("location");
|
||||||
|
let filter = gtk::SearchEntry::builder()
|
||||||
|
.placeholder_text("Filter")
|
||||||
|
.width_chars(18)
|
||||||
|
.build();
|
||||||
|
toolbar.append(&back);
|
||||||
|
toolbar.append(&refresh);
|
||||||
|
toolbar.append(&location_entry);
|
||||||
|
toolbar.append(&filter);
|
||||||
|
root.append(&toolbar);
|
||||||
|
|
||||||
|
let body = gtk::Box::new(gtk::Orientation::Horizontal, 0);
|
||||||
|
body.set_vexpand(true);
|
||||||
|
let sidebar = gtk::Box::new(gtk::Orientation::Vertical, 2);
|
||||||
|
sidebar.set_width_request(168);
|
||||||
|
sidebar.add_css_class("sidebar");
|
||||||
|
let places = [
|
||||||
|
("Desktop", home.join("Desktop")),
|
||||||
|
("Documents", home.join("Documents")),
|
||||||
|
("Downloads", home.join("Downloads")),
|
||||||
|
("Pictures", home.join("Pictures")),
|
||||||
|
];
|
||||||
|
for (label, path) in places {
|
||||||
|
let button = gtk::Button::with_label(label);
|
||||||
|
button.set_halign(gtk::Align::Fill);
|
||||||
|
button.add_css_class("place");
|
||||||
|
// Place actions are wired after the browser exists below.
|
||||||
|
button.set_tooltip_text(Some(&path.display().to_string()));
|
||||||
|
button.set_widget_name(&format!("place:{}", path.display()));
|
||||||
|
sidebar.append(&button);
|
||||||
|
}
|
||||||
|
body.append(&sidebar);
|
||||||
|
|
||||||
|
let content = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||||
|
content.set_hexpand(true);
|
||||||
|
content.set_vexpand(true);
|
||||||
|
let headings = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
|
headings.add_css_class("headings");
|
||||||
|
headings.append(&heading("Name", true));
|
||||||
|
headings.append(&heading("Size", false));
|
||||||
|
headings.append(&heading("Modified", false));
|
||||||
|
content.append(&headings);
|
||||||
|
|
||||||
|
let list = gtk::ListBox::new();
|
||||||
|
list.set_selection_mode(gtk::SelectionMode::Single);
|
||||||
|
list.add_css_class("file-list");
|
||||||
|
let scroll = gtk::ScrolledWindow::builder()
|
||||||
|
.child(&list)
|
||||||
|
.hexpand(true)
|
||||||
|
.vexpand(true)
|
||||||
|
.build();
|
||||||
|
content.append(&scroll);
|
||||||
|
body.append(&content);
|
||||||
|
root.append(&body);
|
||||||
|
|
||||||
|
let status = gtk::Label::new(None);
|
||||||
|
status.set_xalign(0.0);
|
||||||
|
status.add_css_class("status");
|
||||||
|
status.set_margin_start(12);
|
||||||
|
status.set_margin_end(12);
|
||||||
|
status.set_margin_top(6);
|
||||||
|
status.set_margin_bottom(6);
|
||||||
|
root.append(&status);
|
||||||
|
window.set_child(Some(&root));
|
||||||
|
|
||||||
|
let browser = Browser {
|
||||||
|
location: Rc::new(RefCell::new(home)),
|
||||||
|
show_hidden: Rc::new(RefCell::new(false)),
|
||||||
|
list: list.clone(),
|
||||||
|
location_entry: location_entry.clone(),
|
||||||
|
status: status.clone(),
|
||||||
|
filter: filter.clone(),
|
||||||
|
};
|
||||||
|
reload(&browser);
|
||||||
|
|
||||||
|
{
|
||||||
|
let b = browser.clone();
|
||||||
|
location_entry.connect_activate(move |entry| {
|
||||||
|
let path = PathBuf::from(entry.text().as_str());
|
||||||
|
navigate(&b, path);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let b = browser.clone();
|
||||||
|
back.connect_clicked(move |_| {
|
||||||
|
let parent = b.location.borrow().parent().map(Path::to_path_buf);
|
||||||
|
if let Some(path) = parent {
|
||||||
|
navigate(&b, path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let b = browser.clone();
|
||||||
|
refresh.connect_clicked(move |_| reload(&b));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let b = browser.clone();
|
||||||
|
filter.connect_search_changed(move |_| apply_filter(&b));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let b = browser.clone();
|
||||||
|
list.connect_row_activated(move |_, row| {
|
||||||
|
if let Some(path) = row.tooltip_text() {
|
||||||
|
navigate(&b, PathBuf::from(path.as_str()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Sidebar uses the path stored on each button; this avoids hard-coded home assumptions.
|
||||||
|
let mut child = sidebar.first_child();
|
||||||
|
while let Some(widget) = child {
|
||||||
|
child = widget.next_sibling();
|
||||||
|
if let Ok(button) = widget.clone().downcast::<gtk::Button>() {
|
||||||
|
let b = browser.clone();
|
||||||
|
let path = button
|
||||||
|
.widget_name()
|
||||||
|
.strip_prefix("place:")
|
||||||
|
.map(PathBuf::from);
|
||||||
|
if let Some(path) = path {
|
||||||
|
button.connect_clicked(move |_| navigate(&b, path.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let b = browser.clone();
|
||||||
|
let keys = gtk::EventControllerKey::new();
|
||||||
|
keys.connect_key_pressed(move |_, key, _, state| {
|
||||||
|
if state.contains(gtk::gdk::ModifierType::CONTROL_MASK) && key == gtk::gdk::Key::l {
|
||||||
|
b.location_entry.grab_focus();
|
||||||
|
b.location_entry.select_region(0, -1);
|
||||||
|
return glib::Propagation::Stop;
|
||||||
|
}
|
||||||
|
if key == gtk::gdk::Key::F5 {
|
||||||
|
reload(&b);
|
||||||
|
return glib::Propagation::Stop;
|
||||||
|
}
|
||||||
|
if state.contains(gtk::gdk::ModifierType::CONTROL_MASK) && key == gtk::gdk::Key::h {
|
||||||
|
let visible = !*b.show_hidden.borrow();
|
||||||
|
*b.show_hidden.borrow_mut() = visible;
|
||||||
|
reload(&b);
|
||||||
|
return glib::Propagation::Stop;
|
||||||
|
}
|
||||||
|
if key == gtk::gdk::Key::BackSpace {
|
||||||
|
if let Some(parent) = b.location.borrow().parent().map(Path::to_path_buf) {
|
||||||
|
navigate(&b, parent);
|
||||||
|
}
|
||||||
|
return glib::Propagation::Stop;
|
||||||
|
}
|
||||||
|
glib::Propagation::Proceed
|
||||||
|
});
|
||||||
|
window.add_controller(keys);
|
||||||
|
}
|
||||||
|
window.present();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn navigate(browser: &Browser, path: PathBuf) {
|
||||||
|
if path.is_dir() {
|
||||||
|
*browser.location.borrow_mut() = path;
|
||||||
|
reload(browser);
|
||||||
|
} else if path.is_file() {
|
||||||
|
match open_file(&path) {
|
||||||
|
Ok(()) => browser.status.set_text(&format!(
|
||||||
|
"Opened {}",
|
||||||
|
path.file_name().unwrap_or_default().to_string_lossy()
|
||||||
|
)),
|
||||||
|
Err(error) => browser
|
||||||
|
.status
|
||||||
|
.set_text(&format!("Cannot open {}: {error}", path.display())),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
browser
|
||||||
|
.status
|
||||||
|
.set_text("That path does not exist or cannot be opened.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_file(path: &Path) -> Result<(), String> {
|
||||||
|
let extension = path
|
||||||
|
.extension()
|
||||||
|
.and_then(|value| value.to_str())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_ascii_lowercase();
|
||||||
|
let text_extensions = [
|
||||||
|
"txt", "md", "markdown", "rs", "toml", "json", "yaml", "yml", "ini", "conf", "log", "sh",
|
||||||
|
"bash", "zsh", "fish", "c", "h", "cc", "cpp", "hpp", "py", "html", "css", "js", "ts",
|
||||||
|
"xml", "csv", "lock", "desktop", "service", "lua", "nix",
|
||||||
|
];
|
||||||
|
let image_extensions = [
|
||||||
|
"png", "jpg", "jpeg", "gif", "webp", "bmp", "tif", "tiff", "svg", "avif",
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut command = if text_extensions.contains(&extension.as_str()) {
|
||||||
|
let mut command = Command::new("foot");
|
||||||
|
command.args(["-e", "nvim", "--"]).arg(path);
|
||||||
|
command
|
||||||
|
} else if image_extensions.contains(&extension.as_str()) {
|
||||||
|
let mut command = Command::new("imv");
|
||||||
|
command.arg(path);
|
||||||
|
command
|
||||||
|
} else if fs::metadata(path)
|
||||||
|
.map(|metadata| metadata.permissions().mode() & 0o111 != 0)
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
let mut command = Command::new("foot");
|
||||||
|
command.arg("-e").arg(path);
|
||||||
|
command
|
||||||
|
} else {
|
||||||
|
let uri = gio::File::for_path(path).uri();
|
||||||
|
return gio::AppInfo::launch_default_for_uri(&uri, None::<&gio::AppLaunchContext>)
|
||||||
|
.map_err(|error| error.to_string());
|
||||||
|
};
|
||||||
|
|
||||||
|
command
|
||||||
|
.spawn()
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|error| format!("Could not start the requested application: {error}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reload(browser: &Browser) {
|
||||||
|
while let Some(row) = browser.list.first_child() {
|
||||||
|
browser.list.remove(&row);
|
||||||
|
}
|
||||||
|
let path = browser.location.borrow().clone();
|
||||||
|
browser.location_entry.set_text(&path.display().to_string());
|
||||||
|
let entries = match fs::read_dir(&path) {
|
||||||
|
Ok(items) => items,
|
||||||
|
Err(error) => {
|
||||||
|
browser
|
||||||
|
.status
|
||||||
|
.set_text(&format!("Cannot read {}: {error}", path.display()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut rows = Vec::new();
|
||||||
|
for item in entries.flatten() {
|
||||||
|
let path = item.path();
|
||||||
|
let name = item.file_name().to_string_lossy().into_owned();
|
||||||
|
if !*browser.show_hidden.borrow() && name.starts_with('.') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Ok(meta) = item.metadata() else { continue };
|
||||||
|
let directory = meta.is_dir();
|
||||||
|
let modified = meta
|
||||||
|
.modified()
|
||||||
|
.ok()
|
||||||
|
.map(format_time)
|
||||||
|
.unwrap_or_else(|| "—".into());
|
||||||
|
rows.push(Entry {
|
||||||
|
path,
|
||||||
|
name,
|
||||||
|
directory,
|
||||||
|
size: meta.len(),
|
||||||
|
modified,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
rows.sort_by(|a, b| {
|
||||||
|
b.directory
|
||||||
|
.cmp(&a.directory)
|
||||||
|
.then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))
|
||||||
|
});
|
||||||
|
for entry in rows {
|
||||||
|
let row = make_row(&entry);
|
||||||
|
row.set_selectable(true);
|
||||||
|
row.set_activatable(true);
|
||||||
|
row.set_tooltip_text(Some(&entry.path.to_string_lossy()));
|
||||||
|
browser.list.append(&row);
|
||||||
|
}
|
||||||
|
let count = browser.list.observe_children().n_items();
|
||||||
|
browser
|
||||||
|
.status
|
||||||
|
.set_text(&format!("{count} items · {}", path.display()));
|
||||||
|
apply_filter(browser);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_row(entry: &Entry) -> gtk::ListBoxRow {
|
||||||
|
let row = gtk::ListBoxRow::new();
|
||||||
|
let line = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
|
line.set_margin_start(8);
|
||||||
|
line.set_margin_end(8);
|
||||||
|
line.set_margin_top(5);
|
||||||
|
line.set_margin_bottom(5);
|
||||||
|
let name = gtk::Label::new(None);
|
||||||
|
let display_name: String = entry
|
||||||
|
.name
|
||||||
|
.chars()
|
||||||
|
.map(|character| {
|
||||||
|
if character.is_control() {
|
||||||
|
'�'
|
||||||
|
} else {
|
||||||
|
character
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
name.set_text(&if entry.directory {
|
||||||
|
format!("{display_name}/")
|
||||||
|
} else {
|
||||||
|
display_name
|
||||||
|
});
|
||||||
|
name.set_xalign(0.0);
|
||||||
|
name.set_wrap(true);
|
||||||
|
name.set_wrap_mode(gtk::pango::WrapMode::WordChar);
|
||||||
|
name.set_valign(gtk::Align::Center);
|
||||||
|
name.set_hexpand(true);
|
||||||
|
name.add_css_class(if entry.directory {
|
||||||
|
"directory"
|
||||||
|
} else {
|
||||||
|
"filename"
|
||||||
|
});
|
||||||
|
let size_text = if entry.directory {
|
||||||
|
"Folder".to_string()
|
||||||
|
} else {
|
||||||
|
format_size(entry.size)
|
||||||
|
};
|
||||||
|
let size = metadata_value(&size_text, 72);
|
||||||
|
let modified = metadata_value(&entry.modified, 126);
|
||||||
|
line.append(&name);
|
||||||
|
line.append(&size);
|
||||||
|
line.append(&modified);
|
||||||
|
row.set_child(Some(&line));
|
||||||
|
row
|
||||||
|
}
|
||||||
|
|
||||||
|
fn heading(text: &str, expand: bool) -> gtk::Label {
|
||||||
|
let label = gtk::Label::new(Some(text));
|
||||||
|
label.set_xalign(0.0);
|
||||||
|
if expand {
|
||||||
|
label.set_hexpand(true);
|
||||||
|
} else {
|
||||||
|
label.set_size_request(if text == "Size" { 72 } else { 126 }, -1);
|
||||||
|
}
|
||||||
|
label.add_css_class("column-heading");
|
||||||
|
label
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata_value(text: &str, width: i32) -> gtk::Label {
|
||||||
|
let value = gtk::Label::new(Some(text));
|
||||||
|
value.set_size_request(width, 24);
|
||||||
|
value.set_xalign(0.0);
|
||||||
|
value.set_valign(gtk::Align::Center);
|
||||||
|
value.set_ellipsize(gtk::pango::EllipsizeMode::End);
|
||||||
|
value.add_css_class("metadata");
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tool_button(label: &str, tip: &str) -> gtk::Button {
|
||||||
|
let button = gtk::Button::with_label(label);
|
||||||
|
button.set_tooltip_text(Some(tip));
|
||||||
|
button.set_size_request(34, 34);
|
||||||
|
button.add_css_class("tool-button");
|
||||||
|
button
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_filter(browser: &Browser) {
|
||||||
|
let needle = browser.filter.text().to_lowercase();
|
||||||
|
let mut child = browser.list.first_child();
|
||||||
|
while let Some(widget) = child {
|
||||||
|
child = widget.next_sibling();
|
||||||
|
if let Ok(row) = widget.clone().downcast::<gtk::ListBoxRow>() {
|
||||||
|
let name = row
|
||||||
|
.child()
|
||||||
|
.and_then(|line| line.first_child())
|
||||||
|
.and_then(|label| label.downcast::<gtk::Label>().ok())
|
||||||
|
.map(|label| label.text().to_string().to_lowercase())
|
||||||
|
.unwrap_or_default();
|
||||||
|
row.set_visible(name.contains(&needle));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_size(bytes: u64) -> String {
|
||||||
|
const UNITS: [&str; 4] = ["B", "KiB", "MiB", "GiB"];
|
||||||
|
let mut value = bytes as f64;
|
||||||
|
let mut unit = 0;
|
||||||
|
while value >= 1024.0 && unit < UNITS.len() - 1 {
|
||||||
|
value /= 1024.0;
|
||||||
|
unit += 1;
|
||||||
|
}
|
||||||
|
if unit == 0 {
|
||||||
|
format!("{bytes} B")
|
||||||
|
} else {
|
||||||
|
format!("{value:.1} {}", UNITS[unit])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_time(time: std::time::SystemTime) -> String {
|
||||||
|
// Avoid adding a date/time dependency; present a stable age hint when local formatting is unavailable.
|
||||||
|
match time.elapsed() {
|
||||||
|
Ok(age) if age.as_secs() < 60 => "Just now".into(),
|
||||||
|
Ok(age) if age.as_secs() < 3600 => format!("{} min ago", age.as_secs() / 60),
|
||||||
|
Ok(age) if age.as_secs() < 86400 => format!("{} hr ago", age.as_secs() / 3600),
|
||||||
|
Ok(age) => format!("{} days ago", age.as_secs() / 86400),
|
||||||
|
Err(_) => "Just now".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_style() {
|
||||||
|
let provider = gtk::CssProvider::new();
|
||||||
|
provider.load_from_data(r#"
|
||||||
|
* { font-family: "Maple Mono", "JetBrainsMono Nerd Font", monospace; font-size: 10pt; }
|
||||||
|
window, .root { background: #151515; color: #D0D0D0; }
|
||||||
|
.toolbar { background: #181818; padding: 8px 10px; }
|
||||||
|
entry, searchentry { color: #D0D0D0; background: #202020; border: 1px solid #2F2F2F; border-radius: 2px; padding: 7px 9px; min-height: 18px; }
|
||||||
|
entry:focus, searchentry:focus { border-color: #6C99BB; }
|
||||||
|
.tool-button { color: #D0D0D0; background: #202020; border: 1px solid #2F2F2F; border-radius: 2px; padding: 0; min-width: 34px; min-height: 34px; }
|
||||||
|
.back-button { min-width: 34px; min-height: 34px; }
|
||||||
|
.tool-button:hover { color: #F5F5F5; background: #303030; border-color: #454545; }
|
||||||
|
.tool-button:focus-visible, .place:focus-visible, row:focus-visible { outline: 1px solid #6C99BB; outline-offset: -2px; }
|
||||||
|
.sidebar { background: #181818; border-top: 2px solid #2F2F2F; border-right: 1px solid #2F2F2F; padding: 8px 6px; }
|
||||||
|
.place { color: #D0D0D0; background: transparent; border: 1px solid transparent; border-radius: 2px; padding: 7px 9px; min-height: 20px; text-align: left; }
|
||||||
|
.place:hover { color: #F5F5F5; background: #202020; border-color: #2F2F2F; }
|
||||||
|
.place:active { background: #303030; }
|
||||||
|
.headings { background: #181818; border-top: 2px solid #2F2F2F; border-bottom: 1px solid #2F2F2F; padding: 9px 8px; }
|
||||||
|
.column-heading { color: #757575; font-size: 9pt; }
|
||||||
|
.file-list { background: #151515; }
|
||||||
|
.file-list row { border-bottom: 1px solid #202020; min-height: 36px; }
|
||||||
|
.file-list row:hover { background: #202020; }
|
||||||
|
.file-list row:selected { background: #303030; outline: 1px solid #6C99BB; outline-offset: -1px; }
|
||||||
|
.directory { color: #8DB3D0; }
|
||||||
|
.filename { color: #D0D0D0; }
|
||||||
|
.metadata, .status { color: #858585; font-size: 10pt; }
|
||||||
|
.metadata { padding: 3px 0 5px; }
|
||||||
|
scrolledwindow { border: 0; }
|
||||||
|
scrollbar slider { background: #454545; min-width: 7px; min-height: 24px; border-radius: 2px; }
|
||||||
|
"#);
|
||||||
|
if let Some(display) = gtk::gdk::Display::default() {
|
||||||
|
gtk::style_context_add_provider_for_display(
|
||||||
|
&display,
|
||||||
|
&provider,
|
||||||
|
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,929 @@
|
|||||||
|
use gtk::gio;
|
||||||
|
use gtk::glib;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use std::cell::Cell;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::fs;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::{Child, Command};
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Entry {
|
||||||
|
path: PathBuf,
|
||||||
|
name: String,
|
||||||
|
directory: bool,
|
||||||
|
size: u64,
|
||||||
|
modified: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Browser {
|
||||||
|
location: Rc<RefCell<PathBuf>>,
|
||||||
|
show_hidden: Rc<RefCell<bool>>,
|
||||||
|
list: gtk::ListBox,
|
||||||
|
location_entry: gtk::Entry,
|
||||||
|
status: gtk::Label,
|
||||||
|
filter: gtk::SearchEntry,
|
||||||
|
preview_title: gtk::Label,
|
||||||
|
preview_details: gtk::Label,
|
||||||
|
preview_panel: gtk::Box,
|
||||||
|
preview_stack: gtk::Stack,
|
||||||
|
preview_text: gtk::TextView,
|
||||||
|
preview_picture: gtk::Picture,
|
||||||
|
preview_message: gtk::Label,
|
||||||
|
preview_request: Rc<Cell<u64>>,
|
||||||
|
preview_job: Rc<RefCell<Option<PreviewJob>>>,
|
||||||
|
preview_file: Rc<RefCell<Option<PathBuf>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PreviewJob {
|
||||||
|
request: u64,
|
||||||
|
child: Child,
|
||||||
|
output: PathBuf,
|
||||||
|
started: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let app = gtk::Application::builder()
|
||||||
|
.application_id("dev.wfalkowski.Wayfinder")
|
||||||
|
.flags(gio::ApplicationFlags::NON_UNIQUE)
|
||||||
|
.build();
|
||||||
|
app.connect_activate(build_ui);
|
||||||
|
app.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_ui(app: >k::Application) {
|
||||||
|
install_style();
|
||||||
|
let home = std::env::var_os("HOME")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|| PathBuf::from("/"));
|
||||||
|
let window = gtk::ApplicationWindow::builder()
|
||||||
|
.application(app)
|
||||||
|
.title("Wayfinder")
|
||||||
|
.default_width(1080)
|
||||||
|
.default_height(700)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let root = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||||
|
root.add_css_class("root");
|
||||||
|
let toolbar = gtk::Box::new(gtk::Orientation::Horizontal, 6);
|
||||||
|
toolbar.add_css_class("toolbar");
|
||||||
|
|
||||||
|
let back = tool_button("", "Go to parent folder");
|
||||||
|
let back_icon = gtk::Image::from_icon_name("go-previous");
|
||||||
|
back_icon.set_pixel_size(16);
|
||||||
|
back_icon.set_halign(gtk::Align::Center);
|
||||||
|
back_icon.set_valign(gtk::Align::Center);
|
||||||
|
back.set_child(Some(&back_icon));
|
||||||
|
back.add_css_class("back-button");
|
||||||
|
let refresh = tool_button("↻", "Reload this folder");
|
||||||
|
let location_entry = gtk::Entry::builder()
|
||||||
|
.hexpand(true)
|
||||||
|
.placeholder_text("Enter a folder path")
|
||||||
|
.build();
|
||||||
|
location_entry.add_css_class("location");
|
||||||
|
let filter = gtk::SearchEntry::builder()
|
||||||
|
.placeholder_text("Filter")
|
||||||
|
.width_chars(18)
|
||||||
|
.build();
|
||||||
|
toolbar.append(&back);
|
||||||
|
toolbar.append(&refresh);
|
||||||
|
toolbar.append(&location_entry);
|
||||||
|
toolbar.append(&filter);
|
||||||
|
root.append(&toolbar);
|
||||||
|
|
||||||
|
let body = gtk::Box::new(gtk::Orientation::Horizontal, 0);
|
||||||
|
body.set_vexpand(true);
|
||||||
|
let sidebar = gtk::Box::new(gtk::Orientation::Vertical, 2);
|
||||||
|
sidebar.set_width_request(168);
|
||||||
|
sidebar.set_hexpand(false);
|
||||||
|
sidebar.add_css_class("sidebar");
|
||||||
|
let places = [
|
||||||
|
("Desktop", home.join("Desktop")),
|
||||||
|
("Documents", home.join("Documents")),
|
||||||
|
("Downloads", home.join("Downloads")),
|
||||||
|
("Pictures", home.join("Pictures")),
|
||||||
|
];
|
||||||
|
for (label, path) in places {
|
||||||
|
let button = gtk::Button::with_label(label);
|
||||||
|
button.set_halign(gtk::Align::Fill);
|
||||||
|
if let Some(label) = button
|
||||||
|
.child()
|
||||||
|
.and_then(|child| child.downcast::<gtk::Label>().ok())
|
||||||
|
{
|
||||||
|
label.set_xalign(0.0);
|
||||||
|
label.set_hexpand(true);
|
||||||
|
}
|
||||||
|
button.add_css_class("place");
|
||||||
|
// Place actions are wired after the browser exists below.
|
||||||
|
button.set_tooltip_text(Some(&path.display().to_string()));
|
||||||
|
button.set_widget_name(&format!("place:{}", path.display()));
|
||||||
|
sidebar.append(&button);
|
||||||
|
}
|
||||||
|
body.append(&sidebar);
|
||||||
|
|
||||||
|
let content = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||||
|
content.set_hexpand(true);
|
||||||
|
content.set_vexpand(true);
|
||||||
|
let headings = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
|
headings.add_css_class("headings");
|
||||||
|
headings.append(&heading("Name", true));
|
||||||
|
headings.append(&heading("Size", false));
|
||||||
|
headings.append(&heading("Modified", false));
|
||||||
|
content.append(&headings);
|
||||||
|
|
||||||
|
let list = gtk::ListBox::new();
|
||||||
|
list.set_selection_mode(gtk::SelectionMode::Single);
|
||||||
|
list.set_activate_on_single_click(false);
|
||||||
|
list.add_css_class("file-list");
|
||||||
|
let scroll = gtk::ScrolledWindow::builder()
|
||||||
|
.child(&list)
|
||||||
|
.hexpand(true)
|
||||||
|
.vexpand(true)
|
||||||
|
.build();
|
||||||
|
content.append(&scroll);
|
||||||
|
|
||||||
|
let preview_heading = gtk::Label::new(Some("Preview"));
|
||||||
|
preview_heading.set_xalign(0.0);
|
||||||
|
preview_heading.add_css_class("preview-heading");
|
||||||
|
let preview_title = gtk::Label::new(Some("Select a file"));
|
||||||
|
preview_title.set_xalign(0.0);
|
||||||
|
preview_title.set_ellipsize(gtk::pango::EllipsizeMode::Middle);
|
||||||
|
preview_title.add_css_class("preview-title");
|
||||||
|
let preview_details = gtk::Label::new(Some("Select a file to preview"));
|
||||||
|
preview_details.set_xalign(0.0);
|
||||||
|
preview_details.set_wrap(true);
|
||||||
|
preview_details.add_css_class("preview-details");
|
||||||
|
|
||||||
|
let preview_stack = gtk::Stack::new();
|
||||||
|
preview_stack.set_hexpand(true);
|
||||||
|
preview_stack.set_vexpand(true);
|
||||||
|
preview_stack.set_visible_child_name("empty");
|
||||||
|
let preview_empty = gtk::Label::new(Some("Select a file to preview"));
|
||||||
|
preview_empty.set_wrap(true);
|
||||||
|
preview_empty.add_css_class("preview-message");
|
||||||
|
preview_stack.add_named(&preview_empty, Some("empty"));
|
||||||
|
|
||||||
|
let preview_text = gtk::TextView::new();
|
||||||
|
preview_text.set_editable(false);
|
||||||
|
preview_text.set_cursor_visible(false);
|
||||||
|
preview_text.set_monospace(true);
|
||||||
|
preview_text.set_wrap_mode(gtk::WrapMode::None);
|
||||||
|
preview_text.add_css_class("preview-text");
|
||||||
|
let text_scroll = gtk::ScrolledWindow::builder()
|
||||||
|
.child(&preview_text)
|
||||||
|
.hexpand(true)
|
||||||
|
.vexpand(true)
|
||||||
|
.build();
|
||||||
|
preview_stack.add_named(&text_scroll, Some("text"));
|
||||||
|
|
||||||
|
let preview_picture = gtk::Picture::new();
|
||||||
|
preview_picture.set_can_shrink(true);
|
||||||
|
preview_picture.set_keep_aspect_ratio(true);
|
||||||
|
preview_picture.set_halign(gtk::Align::Center);
|
||||||
|
preview_picture.set_valign(gtk::Align::Center);
|
||||||
|
preview_picture.set_hexpand(true);
|
||||||
|
preview_picture.set_vexpand(true);
|
||||||
|
preview_picture.add_css_class("preview-picture");
|
||||||
|
preview_stack.add_named(&preview_picture, Some("image"));
|
||||||
|
|
||||||
|
let preview_message = gtk::Label::new(None);
|
||||||
|
preview_message.set_wrap(true);
|
||||||
|
preview_message.set_justify(gtk::Justification::Center);
|
||||||
|
preview_message.add_css_class("preview-message");
|
||||||
|
preview_stack.add_named(&preview_message, Some("message"));
|
||||||
|
|
||||||
|
let preview = gtk::Box::new(gtk::Orientation::Vertical, 10);
|
||||||
|
preview.set_width_request(320);
|
||||||
|
preview.set_hexpand(false);
|
||||||
|
preview.set_vexpand(true);
|
||||||
|
preview.set_visible(false);
|
||||||
|
preview.add_css_class("preview-pane");
|
||||||
|
preview.append(&preview_heading);
|
||||||
|
preview.append(&preview_title);
|
||||||
|
preview.append(&preview_details);
|
||||||
|
preview.append(&preview_stack);
|
||||||
|
|
||||||
|
let workspace = gtk::Paned::new(gtk::Orientation::Horizontal);
|
||||||
|
workspace.set_start_child(Some(&content));
|
||||||
|
workspace.set_end_child(Some(&preview));
|
||||||
|
workspace.set_position(620);
|
||||||
|
workspace.set_hexpand(true);
|
||||||
|
workspace.set_vexpand(true);
|
||||||
|
workspace.set_resize_start_child(true);
|
||||||
|
workspace.set_resize_end_child(false);
|
||||||
|
workspace.set_shrink_start_child(false);
|
||||||
|
workspace.set_shrink_end_child(false);
|
||||||
|
workspace.add_css_class("workspace-split");
|
||||||
|
body.append(&workspace);
|
||||||
|
root.append(&body);
|
||||||
|
|
||||||
|
let status = gtk::Label::new(None);
|
||||||
|
status.set_xalign(0.0);
|
||||||
|
status.add_css_class("status");
|
||||||
|
status.set_margin_start(12);
|
||||||
|
status.set_margin_end(12);
|
||||||
|
status.set_margin_top(6);
|
||||||
|
status.set_margin_bottom(6);
|
||||||
|
root.append(&status);
|
||||||
|
window.set_child(Some(&root));
|
||||||
|
|
||||||
|
let browser = Browser {
|
||||||
|
location: Rc::new(RefCell::new(home)),
|
||||||
|
show_hidden: Rc::new(RefCell::new(false)),
|
||||||
|
list: list.clone(),
|
||||||
|
location_entry: location_entry.clone(),
|
||||||
|
status: status.clone(),
|
||||||
|
filter: filter.clone(),
|
||||||
|
preview_title,
|
||||||
|
preview_details,
|
||||||
|
preview_panel: preview,
|
||||||
|
preview_stack,
|
||||||
|
preview_text,
|
||||||
|
preview_picture,
|
||||||
|
preview_message,
|
||||||
|
preview_request: Rc::new(Cell::new(0)),
|
||||||
|
preview_job: Rc::new(RefCell::new(None)),
|
||||||
|
preview_file: Rc::new(RefCell::new(None)),
|
||||||
|
};
|
||||||
|
reload(&browser);
|
||||||
|
|
||||||
|
{
|
||||||
|
let b = browser.clone();
|
||||||
|
location_entry.connect_activate(move |entry| {
|
||||||
|
let path = PathBuf::from(entry.text().as_str());
|
||||||
|
navigate(&b, path);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let b = browser.clone();
|
||||||
|
back.connect_clicked(move |_| {
|
||||||
|
let parent = b.location.borrow().parent().map(Path::to_path_buf);
|
||||||
|
if let Some(path) = parent {
|
||||||
|
navigate(&b, path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let b = browser.clone();
|
||||||
|
refresh.connect_clicked(move |_| reload(&b));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let b = browser.clone();
|
||||||
|
filter.connect_search_changed(move |_| apply_filter(&b));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let b = browser.clone();
|
||||||
|
list.connect_row_selected(move |_, row| {
|
||||||
|
if let Some(path) = row
|
||||||
|
.and_then(|row| row.tooltip_text())
|
||||||
|
.map(|path| PathBuf::from(path.as_str()))
|
||||||
|
{
|
||||||
|
show_preview(&b, path);
|
||||||
|
} else {
|
||||||
|
clear_preview(&b);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let b = browser.clone();
|
||||||
|
list.connect_row_activated(move |_, row| {
|
||||||
|
if let Some(path) = row.tooltip_text() {
|
||||||
|
navigate(&b, PathBuf::from(path.as_str()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Sidebar uses the path stored on each button; this avoids hard-coded home assumptions.
|
||||||
|
let mut child = sidebar.first_child();
|
||||||
|
while let Some(widget) = child {
|
||||||
|
child = widget.next_sibling();
|
||||||
|
if let Ok(button) = widget.clone().downcast::<gtk::Button>() {
|
||||||
|
let b = browser.clone();
|
||||||
|
let path = button
|
||||||
|
.widget_name()
|
||||||
|
.strip_prefix("place:")
|
||||||
|
.map(PathBuf::from);
|
||||||
|
if let Some(path) = path {
|
||||||
|
button.connect_clicked(move |_| navigate(&b, path.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let b = browser.clone();
|
||||||
|
let keys = gtk::EventControllerKey::new();
|
||||||
|
keys.connect_key_pressed(move |_, key, _, state| {
|
||||||
|
if state.contains(gtk::gdk::ModifierType::CONTROL_MASK) && key == gtk::gdk::Key::l {
|
||||||
|
b.location_entry.grab_focus();
|
||||||
|
b.location_entry.select_region(0, -1);
|
||||||
|
return glib::Propagation::Stop;
|
||||||
|
}
|
||||||
|
if key == gtk::gdk::Key::F5 {
|
||||||
|
reload(&b);
|
||||||
|
return glib::Propagation::Stop;
|
||||||
|
}
|
||||||
|
if state.contains(gtk::gdk::ModifierType::CONTROL_MASK) && key == gtk::gdk::Key::h {
|
||||||
|
let visible = !*b.show_hidden.borrow();
|
||||||
|
*b.show_hidden.borrow_mut() = visible;
|
||||||
|
reload(&b);
|
||||||
|
return glib::Propagation::Stop;
|
||||||
|
}
|
||||||
|
if key == gtk::gdk::Key::BackSpace {
|
||||||
|
if let Some(parent) = b.location.borrow().parent().map(Path::to_path_buf) {
|
||||||
|
navigate(&b, parent);
|
||||||
|
}
|
||||||
|
return glib::Propagation::Stop;
|
||||||
|
}
|
||||||
|
glib::Propagation::Proceed
|
||||||
|
});
|
||||||
|
window.add_controller(keys);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let b = browser.clone();
|
||||||
|
window.connect_close_request(move |_| {
|
||||||
|
clear_preview(&b);
|
||||||
|
glib::Propagation::Proceed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
window.present();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn navigate(browser: &Browser, path: PathBuf) {
|
||||||
|
if path.is_dir() {
|
||||||
|
*browser.location.borrow_mut() = path;
|
||||||
|
reload(browser);
|
||||||
|
} else if path.is_file() {
|
||||||
|
match open_file(&path) {
|
||||||
|
Ok(()) => browser.status.set_text(&format!(
|
||||||
|
"Opened {}",
|
||||||
|
path.file_name().unwrap_or_default().to_string_lossy()
|
||||||
|
)),
|
||||||
|
Err(error) => browser
|
||||||
|
.status
|
||||||
|
.set_text(&format!("Cannot open {}: {error}", path.display())),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
browser
|
||||||
|
.status
|
||||||
|
.set_text("That path does not exist or cannot be opened.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_preview(browser: &Browser) {
|
||||||
|
cancel_preview_work(browser);
|
||||||
|
browser.preview_panel.set_visible(false);
|
||||||
|
browser.preview_title.set_text("Select a file");
|
||||||
|
browser.preview_details.set_text("Select a file to preview");
|
||||||
|
browser.preview_stack.set_visible_child_name("empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cancel_preview_work(browser: &Browser) -> u64 {
|
||||||
|
let request = browser.preview_request.get().wrapping_add(1);
|
||||||
|
browser.preview_request.set(request);
|
||||||
|
if let Some(mut job) = browser.preview_job.borrow_mut().take() {
|
||||||
|
let _ = job.child.kill();
|
||||||
|
let _ = job.child.wait();
|
||||||
|
let _ = fs::remove_file(job.output);
|
||||||
|
}
|
||||||
|
browser.preview_picture.set_filename(None::<&Path>);
|
||||||
|
if let Some(path) = browser.preview_file.borrow_mut().take() {
|
||||||
|
let _ = fs::remove_file(path);
|
||||||
|
}
|
||||||
|
request
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_preview(browser: &Browser, path: PathBuf) {
|
||||||
|
if !supports_preview(&path) {
|
||||||
|
clear_preview(browser);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let request = cancel_preview_work(browser);
|
||||||
|
browser.preview_panel.set_visible(true);
|
||||||
|
let name = path
|
||||||
|
.file_name()
|
||||||
|
.map(|name| name.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| path.display().to_string());
|
||||||
|
browser.preview_title.set_text(&name);
|
||||||
|
|
||||||
|
let details = fs::metadata(&path)
|
||||||
|
.map(|metadata| {
|
||||||
|
let modified = metadata
|
||||||
|
.modified()
|
||||||
|
.ok()
|
||||||
|
.map(format_time)
|
||||||
|
.unwrap_or_else(|| "Unknown date".into());
|
||||||
|
format!("{} · {modified}", format_size(metadata.len()))
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|_| path.display().to_string());
|
||||||
|
browser.preview_details.set_text(&details);
|
||||||
|
|
||||||
|
let extension = path
|
||||||
|
.extension()
|
||||||
|
.and_then(|value| value.to_str())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_ascii_lowercase();
|
||||||
|
if is_preview_text(&extension) {
|
||||||
|
show_text_preview(browser, &path);
|
||||||
|
} else if is_preview_image(&extension) {
|
||||||
|
start_image_preview(browser, path, request, details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_preview(path: &Path) -> bool {
|
||||||
|
if path.is_dir() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let extension = path
|
||||||
|
.extension()
|
||||||
|
.and_then(|value| value.to_str())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_ascii_lowercase();
|
||||||
|
is_preview_text(&extension) || is_preview_image(&extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_preview_message(browser: &Browser, message: &str) {
|
||||||
|
browser.preview_message.set_text(message);
|
||||||
|
browser.preview_stack.set_visible_child_name("message");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_preview_text(extension: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
extension,
|
||||||
|
"txt"
|
||||||
|
| "md"
|
||||||
|
| "markdown"
|
||||||
|
| "rs"
|
||||||
|
| "toml"
|
||||||
|
| "json"
|
||||||
|
| "yaml"
|
||||||
|
| "yml"
|
||||||
|
| "ini"
|
||||||
|
| "conf"
|
||||||
|
| "log"
|
||||||
|
| "sh"
|
||||||
|
| "bash"
|
||||||
|
| "zsh"
|
||||||
|
| "fish"
|
||||||
|
| "c"
|
||||||
|
| "h"
|
||||||
|
| "cc"
|
||||||
|
| "cpp"
|
||||||
|
| "hpp"
|
||||||
|
| "py"
|
||||||
|
| "html"
|
||||||
|
| "css"
|
||||||
|
| "js"
|
||||||
|
| "ts"
|
||||||
|
| "xml"
|
||||||
|
| "csv"
|
||||||
|
| "lock"
|
||||||
|
| "desktop"
|
||||||
|
| "service"
|
||||||
|
| "lua"
|
||||||
|
| "nix"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_preview_image(extension: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
extension,
|
||||||
|
"png" | "jpg" | "jpeg" | "gif" | "webp" | "bmp" | "tif" | "tiff" | "svg" | "avif" | "ico"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_text_preview(browser: &Browser, path: &Path) {
|
||||||
|
const PREVIEW_LIMIT: usize = 64 * 1024;
|
||||||
|
let mut bytes = Vec::with_capacity(PREVIEW_LIMIT + 1);
|
||||||
|
let result = fs::File::open(path).and_then(|file| {
|
||||||
|
file.take((PREVIEW_LIMIT + 1) as u64)
|
||||||
|
.read_to_end(&mut bytes)
|
||||||
|
});
|
||||||
|
if let Err(error) = result {
|
||||||
|
set_preview_message(browser, &format!("Cannot read this file: {error}"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if bytes.contains(&0) {
|
||||||
|
set_preview_message(
|
||||||
|
browser,
|
||||||
|
"This file contains binary data and cannot be previewed as text.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let truncated = bytes.len() > PREVIEW_LIMIT;
|
||||||
|
bytes.truncate(PREVIEW_LIMIT);
|
||||||
|
let mut text = String::from_utf8_lossy(&bytes).into_owned();
|
||||||
|
if truncated {
|
||||||
|
text.push_str("\n\n[Preview limited to the first 64 KiB]");
|
||||||
|
}
|
||||||
|
browser.preview_text.buffer().set_text(&text);
|
||||||
|
browser.preview_stack.set_visible_child_name("text");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_image_preview(browser: &Browser, path: PathBuf, request: u64, details: String) {
|
||||||
|
let source = match fs::canonicalize(&path) {
|
||||||
|
Ok(source) => source,
|
||||||
|
Err(error) => {
|
||||||
|
set_preview_message(browser, &format!("Cannot open this image: {error}"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let output_dir = std::env::temp_dir().join(format!("wayfinder-preview-{}", std::process::id()));
|
||||||
|
if let Err(error) = fs::create_dir_all(&output_dir) {
|
||||||
|
set_preview_message(browser, &format!("Cannot create preview cache: {error}"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Err(error) = fs::set_permissions(&output_dir, fs::Permissions::from_mode(0o700)) {
|
||||||
|
set_preview_message(browser, &format!("Cannot secure preview cache: {error}"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let output = output_dir.join(format!("preview-{request}.png"));
|
||||||
|
let _ = fs::remove_file(&output);
|
||||||
|
let extension = path
|
||||||
|
.extension()
|
||||||
|
.and_then(|value| value.to_str())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let sandbox_input = format!("/wayfinder-input/preview.{extension}");
|
||||||
|
let mut command = Command::new("bwrap");
|
||||||
|
command
|
||||||
|
.args([
|
||||||
|
"--unshare-user",
|
||||||
|
"--unshare-pid",
|
||||||
|
"--unshare-ipc",
|
||||||
|
"--unshare-uts",
|
||||||
|
"--die-with-parent",
|
||||||
|
"--new-session",
|
||||||
|
"--ro-bind",
|
||||||
|
"/usr",
|
||||||
|
"/usr",
|
||||||
|
"--ro-bind",
|
||||||
|
"/etc",
|
||||||
|
"/etc",
|
||||||
|
"--ro-bind-try",
|
||||||
|
"/lib",
|
||||||
|
"/lib",
|
||||||
|
"--ro-bind-try",
|
||||||
|
"/lib64",
|
||||||
|
"/lib64",
|
||||||
|
"--dev",
|
||||||
|
"/dev",
|
||||||
|
"--proc",
|
||||||
|
"/proc",
|
||||||
|
"--tmpfs",
|
||||||
|
"/tmp",
|
||||||
|
"--dir",
|
||||||
|
"/wayfinder-input",
|
||||||
|
"--ro-bind",
|
||||||
|
])
|
||||||
|
.arg(&source)
|
||||||
|
.arg(&sandbox_input)
|
||||||
|
.arg("--bind")
|
||||||
|
.arg(&output_dir)
|
||||||
|
.args([
|
||||||
|
"/wayfinder-output",
|
||||||
|
"--chdir",
|
||||||
|
"/",
|
||||||
|
"--setenv",
|
||||||
|
"HOME",
|
||||||
|
"/tmp",
|
||||||
|
"--",
|
||||||
|
"/usr/bin/gdk-pixbuf-thumbnailer",
|
||||||
|
"--size",
|
||||||
|
"512",
|
||||||
|
])
|
||||||
|
.arg(&sandbox_input)
|
||||||
|
.arg(format!("/wayfinder-output/preview-{request}.png"));
|
||||||
|
command.stdout(std::process::Stdio::null());
|
||||||
|
command.stderr(std::process::Stdio::null());
|
||||||
|
|
||||||
|
match command.spawn() {
|
||||||
|
Ok(child) => {
|
||||||
|
browser
|
||||||
|
.preview_details
|
||||||
|
.set_text("Generating image preview…");
|
||||||
|
*browser.preview_job.borrow_mut() = Some(PreviewJob {
|
||||||
|
request,
|
||||||
|
child,
|
||||||
|
output: output.clone(),
|
||||||
|
started: Instant::now(),
|
||||||
|
});
|
||||||
|
let browser = browser.clone();
|
||||||
|
glib::timeout_add_local(Duration::from_millis(80), move || {
|
||||||
|
if browser.preview_request.get() != request {
|
||||||
|
return glib::ControlFlow::Break;
|
||||||
|
}
|
||||||
|
let completion = {
|
||||||
|
let mut pending = browser.preview_job.borrow_mut();
|
||||||
|
let Some(job) = pending.as_mut() else {
|
||||||
|
return glib::ControlFlow::Break;
|
||||||
|
};
|
||||||
|
if job.request != request {
|
||||||
|
return glib::ControlFlow::Break;
|
||||||
|
}
|
||||||
|
match job.child.try_wait() {
|
||||||
|
Ok(None) if job.started.elapsed() < Duration::from_secs(8) => None,
|
||||||
|
Ok(None) => {
|
||||||
|
let _ = job.child.kill();
|
||||||
|
let _ = job.child.wait();
|
||||||
|
Some(false)
|
||||||
|
}
|
||||||
|
Ok(Some(status)) => Some(status.success()),
|
||||||
|
Err(_) => Some(false),
|
||||||
|
}
|
||||||
|
.map(|success| {
|
||||||
|
let job = pending.take().expect("preview job exists");
|
||||||
|
(success, job.output)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let Some((success, output)) = completion else {
|
||||||
|
return glib::ControlFlow::Continue;
|
||||||
|
};
|
||||||
|
if success && output.is_file() {
|
||||||
|
browser.preview_picture.set_filename(Some(&output));
|
||||||
|
browser.preview_details.set_text(&details);
|
||||||
|
*browser.preview_file.borrow_mut() = Some(output);
|
||||||
|
browser.preview_stack.set_visible_child_name("image");
|
||||||
|
} else {
|
||||||
|
let _ = fs::remove_file(output);
|
||||||
|
set_preview_message(
|
||||||
|
&browser,
|
||||||
|
"Image preview failed. The format may be unsupported, or the sandboxed thumbnailer may be unavailable.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
glib::ControlFlow::Break
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(error) => set_preview_message(
|
||||||
|
browser,
|
||||||
|
&format!("Could not start the sandboxed image preview: {error}"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_file(path: &Path) -> Result<(), String> {
|
||||||
|
let extension = path
|
||||||
|
.extension()
|
||||||
|
.and_then(|value| value.to_str())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_ascii_lowercase();
|
||||||
|
let text_extensions = [
|
||||||
|
"txt", "md", "markdown", "rs", "toml", "json", "yaml", "yml", "ini", "conf", "log", "sh",
|
||||||
|
"bash", "zsh", "fish", "c", "h", "cc", "cpp", "hpp", "py", "html", "css", "js", "ts",
|
||||||
|
"xml", "csv", "lock", "desktop", "service", "lua", "nix",
|
||||||
|
];
|
||||||
|
let image_extensions = [
|
||||||
|
"png", "jpg", "jpeg", "gif", "webp", "bmp", "tif", "tiff", "svg", "avif",
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut command = if text_extensions.contains(&extension.as_str()) {
|
||||||
|
let mut command = Command::new("foot");
|
||||||
|
command.args(["-e", "nvim", "--"]).arg(path);
|
||||||
|
command
|
||||||
|
} else if image_extensions.contains(&extension.as_str()) {
|
||||||
|
let mut command = Command::new("imv");
|
||||||
|
command.arg(path);
|
||||||
|
command
|
||||||
|
} else if fs::metadata(path)
|
||||||
|
.map(|metadata| metadata.permissions().mode() & 0o111 != 0)
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
let mut command = Command::new("foot");
|
||||||
|
command.arg("-e").arg(path);
|
||||||
|
command
|
||||||
|
} else {
|
||||||
|
let uri = gio::File::for_path(path).uri();
|
||||||
|
return gio::AppInfo::launch_default_for_uri(&uri, None::<&gio::AppLaunchContext>)
|
||||||
|
.map_err(|error| error.to_string());
|
||||||
|
};
|
||||||
|
|
||||||
|
command
|
||||||
|
.spawn()
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|error| format!("Could not start the requested application: {error}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reload(browser: &Browser) {
|
||||||
|
clear_preview(browser);
|
||||||
|
while let Some(row) = browser.list.first_child() {
|
||||||
|
browser.list.remove(&row);
|
||||||
|
}
|
||||||
|
let path = browser.location.borrow().clone();
|
||||||
|
browser.location_entry.set_text(&path.display().to_string());
|
||||||
|
let entries = match fs::read_dir(&path) {
|
||||||
|
Ok(items) => items,
|
||||||
|
Err(error) => {
|
||||||
|
browser
|
||||||
|
.status
|
||||||
|
.set_text(&format!("Cannot read {}: {error}", path.display()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut rows = Vec::new();
|
||||||
|
for item in entries.flatten() {
|
||||||
|
let path = item.path();
|
||||||
|
let name = item.file_name().to_string_lossy().into_owned();
|
||||||
|
if !*browser.show_hidden.borrow() && name.starts_with('.') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Ok(meta) = item.metadata() else { continue };
|
||||||
|
let directory = meta.is_dir();
|
||||||
|
let modified = meta
|
||||||
|
.modified()
|
||||||
|
.ok()
|
||||||
|
.map(format_time)
|
||||||
|
.unwrap_or_else(|| "—".into());
|
||||||
|
rows.push(Entry {
|
||||||
|
path,
|
||||||
|
name,
|
||||||
|
directory,
|
||||||
|
size: meta.len(),
|
||||||
|
modified,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
rows.sort_by(|a, b| {
|
||||||
|
b.directory
|
||||||
|
.cmp(&a.directory)
|
||||||
|
.then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))
|
||||||
|
});
|
||||||
|
for entry in rows {
|
||||||
|
let row = make_row(&entry);
|
||||||
|
row.set_selectable(true);
|
||||||
|
row.set_activatable(true);
|
||||||
|
row.set_tooltip_text(Some(&entry.path.to_string_lossy()));
|
||||||
|
browser.list.append(&row);
|
||||||
|
}
|
||||||
|
let count = browser.list.observe_children().n_items();
|
||||||
|
browser
|
||||||
|
.status
|
||||||
|
.set_text(&format!("{count} items · {}", path.display()));
|
||||||
|
apply_filter(browser);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_row(entry: &Entry) -> gtk::ListBoxRow {
|
||||||
|
let row = gtk::ListBoxRow::new();
|
||||||
|
let line = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
|
line.set_margin_start(8);
|
||||||
|
line.set_margin_end(8);
|
||||||
|
line.set_margin_top(5);
|
||||||
|
line.set_margin_bottom(5);
|
||||||
|
let name = gtk::Label::new(None);
|
||||||
|
let display_name: String = entry
|
||||||
|
.name
|
||||||
|
.chars()
|
||||||
|
.map(|character| {
|
||||||
|
if character.is_control() {
|
||||||
|
'�'
|
||||||
|
} else {
|
||||||
|
character
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
name.set_text(&if entry.directory {
|
||||||
|
format!("{display_name}/")
|
||||||
|
} else {
|
||||||
|
display_name
|
||||||
|
});
|
||||||
|
name.set_xalign(0.0);
|
||||||
|
name.set_wrap(true);
|
||||||
|
name.set_wrap_mode(gtk::pango::WrapMode::WordChar);
|
||||||
|
name.set_valign(gtk::Align::Center);
|
||||||
|
name.set_hexpand(true);
|
||||||
|
name.add_css_class(if entry.directory {
|
||||||
|
"directory"
|
||||||
|
} else {
|
||||||
|
"filename"
|
||||||
|
});
|
||||||
|
let size_text = if entry.directory {
|
||||||
|
"Folder".to_string()
|
||||||
|
} else {
|
||||||
|
format_size(entry.size)
|
||||||
|
};
|
||||||
|
let size = metadata_value(&size_text, 72);
|
||||||
|
let modified = metadata_value(&entry.modified, 126);
|
||||||
|
line.append(&name);
|
||||||
|
line.append(&size);
|
||||||
|
line.append(&modified);
|
||||||
|
row.set_child(Some(&line));
|
||||||
|
row
|
||||||
|
}
|
||||||
|
|
||||||
|
fn heading(text: &str, expand: bool) -> gtk::Label {
|
||||||
|
let label = gtk::Label::new(Some(text));
|
||||||
|
label.set_xalign(0.0);
|
||||||
|
if expand {
|
||||||
|
label.set_hexpand(true);
|
||||||
|
} else {
|
||||||
|
label.set_size_request(if text == "Size" { 72 } else { 126 }, -1);
|
||||||
|
}
|
||||||
|
label.add_css_class("column-heading");
|
||||||
|
label
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata_value(text: &str, width: i32) -> gtk::Label {
|
||||||
|
let value = gtk::Label::new(Some(text));
|
||||||
|
value.set_size_request(width, 24);
|
||||||
|
value.set_xalign(0.0);
|
||||||
|
value.set_valign(gtk::Align::Center);
|
||||||
|
value.set_ellipsize(gtk::pango::EllipsizeMode::End);
|
||||||
|
value.add_css_class("metadata");
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tool_button(label: &str, tip: &str) -> gtk::Button {
|
||||||
|
let button = gtk::Button::with_label(label);
|
||||||
|
button.set_tooltip_text(Some(tip));
|
||||||
|
button.set_size_request(34, 34);
|
||||||
|
button.add_css_class("tool-button");
|
||||||
|
button
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_filter(browser: &Browser) {
|
||||||
|
let needle = browser.filter.text().to_lowercase();
|
||||||
|
let mut child = browser.list.first_child();
|
||||||
|
while let Some(widget) = child {
|
||||||
|
child = widget.next_sibling();
|
||||||
|
if let Ok(row) = widget.clone().downcast::<gtk::ListBoxRow>() {
|
||||||
|
let name = row
|
||||||
|
.child()
|
||||||
|
.and_then(|line| line.first_child())
|
||||||
|
.and_then(|label| label.downcast::<gtk::Label>().ok())
|
||||||
|
.map(|label| label.text().to_string().to_lowercase())
|
||||||
|
.unwrap_or_default();
|
||||||
|
row.set_visible(name.contains(&needle));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_size(bytes: u64) -> String {
|
||||||
|
const UNITS: [&str; 4] = ["B", "KiB", "MiB", "GiB"];
|
||||||
|
let mut value = bytes as f64;
|
||||||
|
let mut unit = 0;
|
||||||
|
while value >= 1024.0 && unit < UNITS.len() - 1 {
|
||||||
|
value /= 1024.0;
|
||||||
|
unit += 1;
|
||||||
|
}
|
||||||
|
if unit == 0 {
|
||||||
|
format!("{bytes} B")
|
||||||
|
} else {
|
||||||
|
format!("{value:.1} {}", UNITS[unit])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_time(time: std::time::SystemTime) -> String {
|
||||||
|
// Avoid adding a date/time dependency; present a stable age hint when local formatting is unavailable.
|
||||||
|
match time.elapsed() {
|
||||||
|
Ok(age) if age.as_secs() < 60 => "Just now".into(),
|
||||||
|
Ok(age) if age.as_secs() < 3600 => format!("{} min ago", age.as_secs() / 60),
|
||||||
|
Ok(age) if age.as_secs() < 86400 => format!("{} hr ago", age.as_secs() / 3600),
|
||||||
|
Ok(age) => format!("{} days ago", age.as_secs() / 86400),
|
||||||
|
Err(_) => "Just now".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_style() {
|
||||||
|
let provider = gtk::CssProvider::new();
|
||||||
|
provider.load_from_data(r#"
|
||||||
|
* { font-family: "Maple Mono", "JetBrainsMono Nerd Font", monospace; font-size: 10pt; }
|
||||||
|
window, .root { background: #151515; color: #D0D0D0; }
|
||||||
|
.toolbar { background: #181818; padding: 8px 10px; }
|
||||||
|
entry, searchentry { color: #D0D0D0; background: #202020; border: 1px solid #2F2F2F; border-radius: 2px; padding: 7px 9px; min-height: 18px; }
|
||||||
|
entry:focus, searchentry:focus { border-color: #6C99BB; }
|
||||||
|
.tool-button { color: #D0D0D0; background: #202020; border: 1px solid #2F2F2F; border-radius: 2px; padding: 0; min-width: 34px; min-height: 34px; }
|
||||||
|
.back-button { min-width: 34px; min-height: 34px; }
|
||||||
|
.tool-button:hover { color: #F5F5F5; background: #303030; border-color: #454545; }
|
||||||
|
.tool-button:focus-visible, .place:focus-visible, row:focus-visible { outline: 1px solid #6C99BB; outline-offset: -2px; }
|
||||||
|
.sidebar { background: #181818; border-top: 2px solid #2F2F2F; border-right: 1px solid #2F2F2F; padding: 8px 6px; min-width: 168px; max-width: 168px; }
|
||||||
|
.place { color: #D0D0D0; background: transparent; border: 1px solid transparent; border-radius: 2px; padding: 7px 9px; min-height: 20px; }
|
||||||
|
.place:hover { color: #F5F5F5; background: #202020; border-color: #2F2F2F; }
|
||||||
|
.place:active { background: #303030; }
|
||||||
|
.headings { background: #181818; border-top: 2px solid #2F2F2F; border-bottom: 1px solid #2F2F2F; padding: 9px 8px; }
|
||||||
|
.preview-pane { background: #181818; border-left: 1px solid #2F2F2F; padding: 12px; min-width: 320px; max-width: 320px; }
|
||||||
|
.preview-heading { color: #F5F5F5; font-size: 11pt; }
|
||||||
|
.preview-title { color: #D0D0D0; font-size: 10pt; }
|
||||||
|
.preview-details { color: #757575; font-size: 9pt; }
|
||||||
|
.preview-message { color: #858585; padding: 12px; }
|
||||||
|
.preview-text { background: #151515; color: #D0D0D0; padding: 10px; }
|
||||||
|
.preview-picture { background: #151515; }
|
||||||
|
.workspace-split > separator { background: #2F2F2F; min-width: 1px; }
|
||||||
|
.column-heading { color: #757575; font-size: 9pt; }
|
||||||
|
.file-list { background: #151515; }
|
||||||
|
.file-list row { border-bottom: 1px solid #202020; min-height: 36px; }
|
||||||
|
.file-list row:hover { background: #202020; }
|
||||||
|
.file-list row:selected { background: #303030; outline: 1px solid #6C99BB; outline-offset: -1px; }
|
||||||
|
.directory { color: #8DB3D0; }
|
||||||
|
.filename { color: #D0D0D0; }
|
||||||
|
.metadata, .status { color: #858585; font-size: 10pt; }
|
||||||
|
.metadata { padding: 3px 0 5px; }
|
||||||
|
scrolledwindow { border: 0; }
|
||||||
|
scrollbar slider { background: #454545; min-width: 7px; min-height: 24px; border-radius: 2px; }
|
||||||
|
"#);
|
||||||
|
if let Some(display) = gtk::gdk::Display::default() {
|
||||||
|
gtk::style_context_add_provider_for_display(
|
||||||
|
&display,
|
||||||
|
&provider,
|
||||||
|
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user