diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..ba7355e
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,2295 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bitflags"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
+[[package]]
+name = "cairo-rs"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cc8d9aa793480744cd9a0524fef1a2e197d9eaa0f739cde19d16aba530dcb95"
+dependencies = [
+ "bitflags",
+ "cairo-sys-rs",
+ "glib",
+ "libc",
+]
+
+[[package]]
+name = "cairo-sys-rs"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8b4985713047f5faee02b8db6a6ef32bbb50269ff53c1aee716d1d195b76d54"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-expr"
+version = "0.20.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78cef5b5a1a6827c7322ae2a636368a573006b27cfa76c7ebd53e834daeaab6a"
+dependencies = [
+ "smallvec",
+ "target-lexicon",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "chrono"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+
+[[package]]
+name = "cookie"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
+dependencies = [
+ "percent-encoding",
+ "time",
+ "version_check",
+]
+
+[[package]]
+name = "cookie_store"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206"
+dependencies = [
+ "cookie",
+ "document-features",
+ "idna",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "time",
+ "url",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "document-features"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
+dependencies = [
+ "litrs",
+]
+
+[[package]]
+name = "driftwood"
+version = "0.1.0"
+dependencies = [
+ "chrono",
+ "clap",
+ "dirs",
+ "env_logger",
+ "gio",
+ "glib",
+ "glib-build-tools",
+ "gtk4",
+ "humansize",
+ "libadwaita",
+ "log",
+ "rusqlite",
+ "serde",
+ "serde_json",
+ "sha2",
+ "tempfile",
+ "ureq",
+]
+
+[[package]]
+name = "env_filter"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f"
+dependencies = [
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "env_filter",
+ "jiff",
+ "log",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "fallible-iterator"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
+
+[[package]]
+name = "fallible-streaming-iterator"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[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 = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-core",
+ "futures-macro",
+ "futures-task",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "gdk-pixbuf"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25f420376dbee041b2db374ce4573892a36222bb3f6c0c43e24f0d67eae9b646"
+dependencies = [
+ "gdk-pixbuf-sys",
+ "gio",
+ "glib",
+ "libc",
+]
+
+[[package]]
+name = "gdk-pixbuf-sys"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48f31b37b1fc4b48b54f6b91b7ef04c18e00b4585d98359dd7b998774bbd91fb"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gdk4"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e9a95621b043a35e70ea9f6da89e1a471658ee5771258874f000a11f6a9cb89"
+dependencies = [
+ "cairo-rs",
+ "gdk-pixbuf",
+ "gdk4-sys",
+ "gio",
+ "glib",
+ "libc",
+ "pango",
+]
+
+[[package]]
+name = "gdk4-sys"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d322515677e4a12e10efe7e758743c3e5faa56940237f19ba6890ba8edbbb76"
+dependencies = [
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+ "wasip3",
+]
+
+[[package]]
+name = "gio"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb018ae3f3967134a833fac8a2568c3e698f4b95c658865360772d73803cc5de"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "gio-sys",
+ "glib",
+ "libc",
+ "pin-project-lite",
+ "smallvec",
+]
+
+[[package]]
+name = "gio-sys"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64729ba2772c080448f9f966dba8f4456beeb100d8c28a865ef8a0f2ef4987e1"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "glib"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3b79a62980e85d61aae88988e5bcca6a35f05b0008e1333aa43bd61ef9bf35"
+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-build-tools"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bf3abfd1e60b194dded50f802277c68a59121a5a221701102f02db223cdda27"
+dependencies = [
+ "gio",
+]
+
+[[package]]
+name = "glib-macros"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b48a968528354e04603ae96205f00cf7e77347ffde0fc943a2159948d7d6d80"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "glib-sys"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48073e3b228419faa80b9b7f7122759d4ab2f44cd52a065fde7ca08f34c03147"
+dependencies = [
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gobject-sys"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18eda93f09d3778f38255b231b17ef67195013a592c91624a4daf8bead875565"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "graphene-rs"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7d1b7881f96869f49808b6adfe906a93a57a34204952253444d68c3208d71f1"
+dependencies = [
+ "glib",
+ "graphene-sys",
+ "libc",
+]
+
+[[package]]
+name = "graphene-sys"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "517f062f3fd6b7fd3e57a3f038a74b3c23ca32f51199ff028aa704609943f79c"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gsk4"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c84a7e778764e5c8e67440c616e11a0da21828bbf04490655c4058f6c917af6f"
+dependencies = [
+ "cairo-rs",
+ "gdk4",
+ "glib",
+ "graphene-rs",
+ "gsk4-sys",
+ "libc",
+ "pango",
+]
+
+[[package]]
+name = "gsk4-sys"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fbfbe080ce408e35a94eded4b9cc7a16fa2ffbaf6f04ab5a35d9c3c73841c88"
+dependencies = [
+ "cairo-sys-rs",
+ "gdk4-sys",
+ "glib-sys",
+ "gobject-sys",
+ "graphene-sys",
+ "libc",
+ "pango-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk4"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f78f0fe325e541c40f1a7f647d324bdda34cf67fe217c2843fd9c18ae514c9"
+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.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3581b242ba62fdff122ebb626ea641582ec326031622bd19d60f85029c804a87"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "gtk4-sys"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f063fc314b4d23aac4316a877159c02e86c1b2ba1f0a13e9aafbbebdce9c0800"
+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.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+
+[[package]]
+name = "hashlink"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
+dependencies = [
+ "hashbrown 0.15.5",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "http"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "humansize"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
+dependencies = [
+ "libm",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
+
+[[package]]
+name = "icu_properties"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
+
+[[package]]
+name = "icu_provider"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.16.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itoa"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
+
+[[package]]
+name = "jiff"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3e3d65f018c6ae946ab16e80944b97096ed73c35b221d1c478a6c81d8f57940"
+dependencies = [
+ "jiff-static",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde_core",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a17c2b211d863c7fde02cbea8a3c1a439b98e109286554f2860bdded7ff83818"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.90"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14dc6f6450b3f6d4ed5b16327f38fed626d375a886159ca555bd7822c0c3a5a6"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
+name = "libadwaita"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc0da4e27b20d3e71f830e5b0f0188d22c257986bf421c02cfde777fe07932a4"
+dependencies = [
+ "gdk4",
+ "gio",
+ "glib",
+ "gtk4",
+ "libadwaita-sys",
+ "libc",
+ "pango",
+]
+
+[[package]]
+name = "libadwaita-sys"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aaee067051c5d3c058d050d167688b80b67de1950cfca77730549aa761fc5d7d"
+dependencies = [
+ "gdk4-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "gtk4-sys",
+ "libc",
+ "pango-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.182"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
+
+[[package]]
+name = "libm"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+
+[[package]]
+name = "libredox"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
+dependencies = [
+ "bitflags",
+ "libc",
+]
+
+[[package]]
+name = "libsqlite3-sys"
+version = "0.31.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad8935b44e7c13394a179a438e0cebba0fe08fe01b54f152e29a93b5cf993fd4"
+dependencies = [
+ "cc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "litemap"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
+
+[[package]]
+name = "litrs"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "pango"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25d8f224eddef627b896d2f7b05725b3faedbd140e0e8343446f0d34f34238ee"
+dependencies = [
+ "gio",
+ "glib",
+ "libc",
+ "pango-sys",
+]
+
+[[package]]
+name = "pango-sys"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd111a20ca90fedf03e09c59783c679c00900f1d8491cca5399f5e33609d5d6"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+[[package]]
+name = "portable-atomic"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
+name = "potential_utf"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
+dependencies = [
+ "toml_edit",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "redox_users"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
+dependencies = [
+ "getrandom 0.2.17",
+ "libredox",
+ "thiserror",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rusqlite"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c6d5e5acb6f6129fe3f7ba0a7fc77bca1942cb568535e18e7bc40262baf3110"
+dependencies = [
+ "bitflags",
+ "fallible-iterator",
+ "fallible-streaming-iterator",
+ "hashlink",
+ "libsqlite3-sys",
+ "smallvec",
+]
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
+dependencies = [
+ "log",
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "semver"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "system-deps"
+version = "7.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f"
+dependencies = [
+ "cfg-expr",
+ "heck",
+ "pkg-config",
+ "toml",
+ "version-compare",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c"
+
+[[package]]
+name = "tempfile"
+version = "3.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.1",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "time"
+version = "0.3.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
+
+[[package]]
+name = "time-macros"
+version = "0.2.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "toml"
+version = "0.9.12+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
+dependencies = [
+ "indexmap",
+ "serde_core",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_parser",
+ "toml_writer",
+ "winnow",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.7.5+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.23.10+spec-1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
+dependencies = [
+ "indexmap",
+ "toml_datetime",
+ "toml_parser",
+ "winnow",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.0.9+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
+dependencies = [
+ "winnow",
+]
+
+[[package]]
+name = "toml_writer"
+version = "1.0.6+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
+
+[[package]]
+name = "typenum"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "ureq"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdc97a28575b85cfedf2a7e7d3cc64b3e11bd8ac766666318003abbacc7a21fc"
+dependencies = [
+ "base64",
+ "cookie_store",
+ "flate2",
+ "log",
+ "percent-encoding",
+ "rustls",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "ureq-proto",
+ "utf-8",
+ "webpki-roots",
+]
+
+[[package]]
+name = "ureq-proto"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f"
+dependencies = [
+ "base64",
+ "http",
+ "httparse",
+ "log",
+]
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version-compare"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.2+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.113"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60722a937f594b7fde9adb894d7c092fc1bb6612897c46368d18e7a20208eff2"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.113"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fac8c6395094b6b91c4af293f4c79371c163f9a6f56184d2c9a85f5a95f3950"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.113"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab3fabce6159dc20728033842636887e4877688ae94382766e00b180abac9d60"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.113"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de0e091bdb824da87dc01d967388880d017a0a9bc4f3bdc0d86ee9f9336e3bb5"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "winnow"
+version = "0.7.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "prettyplease",
+ "syn",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
+[[package]]
+name = "writeable"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
+
+[[package]]
+name = "yoke"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..ba3c791
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,46 @@
+[package]
+name = "driftwood"
+version = "0.1.0"
+edition = "2021"
+license = "GPL-3.0-or-later"
+
+[dependencies]
+gtk = { version = "0.11", package = "gtk4", features = ["v4_16"] }
+adw = { version = "0.9", package = "libadwaita", features = ["v1_6"] }
+glib = "0.22"
+gio = "0.22"
+
+# Database
+rusqlite = { version = "0.33", features = ["bundled"] }
+
+# CLI
+clap = { version = "4", features = ["derive"] }
+
+# File hashing
+sha2 = "0.10"
+
+# Time
+chrono = "0.4"
+
+# XDG directories
+dirs = "6"
+
+# Human-readable sizes
+humansize = "2"
+
+# HTTP client (sync, lightweight - for update checks)
+ureq = { version = "3", features = ["json"] }
+
+# JSON parsing
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+
+# Logging
+log = "0.4"
+env_logger = "0.11"
+
+# Temp directories (for AppImage extraction)
+tempfile = "3"
+
+[build-dependencies]
+glib-build-tools = "0.22"
diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000..d7170e8
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,36 @@
+use std::path::PathBuf;
+use std::process::Command;
+
+fn main() {
+ // Compile GResources
+ glib_build_tools::compile_resources(
+ &["data"],
+ "data/resources.gresource.xml",
+ "driftwood.gresource",
+ );
+
+ // Compile GSettings schema for development builds
+ let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
+ let schema_dir = out_dir.join("gschemas");
+ std::fs::create_dir_all(&schema_dir).expect("Failed to create schema dir");
+
+ std::fs::copy(
+ "data/app.driftwood.Driftwood.gschema.xml",
+ schema_dir.join("app.driftwood.Driftwood.gschema.xml"),
+ )
+ .expect("Failed to copy schema");
+
+ let status = Command::new("glib-compile-schemas")
+ .arg(&schema_dir)
+ .status()
+ .expect("Failed to run glib-compile-schemas");
+
+ if !status.success() {
+ panic!("glib-compile-schemas failed");
+ }
+
+ println!(
+ "cargo::rustc-env=GSETTINGS_SCHEMA_DIR={}",
+ schema_dir.display()
+ );
+}
diff --git a/data/app.driftwood.Driftwood.desktop b/data/app.driftwood.Driftwood.desktop
new file mode 100644
index 0000000..3f3de0c
--- /dev/null
+++ b/data/app.driftwood.Driftwood.desktop
@@ -0,0 +1,10 @@
+[Desktop Entry]
+Name=Driftwood
+Comment=Modern AppImage manager for GNOME desktops
+Exec=driftwood
+Icon=app.driftwood.Driftwood
+Terminal=false
+Type=Application
+Categories=System;PackageManager;GTK;
+Keywords=AppImage;Application;Manager;Package;
+StartupNotify=true
diff --git a/data/app.driftwood.Driftwood.gschema.xml b/data/app.driftwood.Driftwood.gschema.xml
new file mode 100644
index 0000000..41c7ca7
--- /dev/null
+++ b/data/app.driftwood.Driftwood.gschema.xml
@@ -0,0 +1,35 @@
+
+
+
+
+ 900
+ Window width
+ The width of the main application window.
+
+
+ 600
+ Window height
+ The height of the main application window.
+
+
+ false
+ Window maximized
+ Whether the main application window is maximized.
+
+
+ ['~/Applications', '~/Downloads']
+ Scan directories
+ Directories to scan for AppImage files.
+
+
+ 'grid'
+ Library view mode
+ The library view mode: grid or list.
+
+
+ 'default'
+ Color scheme
+ Application color scheme: default (follow system), force-light, or force-dark.
+
+
+
diff --git a/data/resources.gresource.xml b/data/resources.gresource.xml
new file mode 100644
index 0000000..e3d1a17
--- /dev/null
+++ b/data/resources.gresource.xml
@@ -0,0 +1,6 @@
+
+
+
+ resources/style.css
+
+
diff --git a/data/resources/style.css b/data/resources/style.css
new file mode 100644
index 0000000..be6c5dc
--- /dev/null
+++ b/data/resources/style.css
@@ -0,0 +1,72 @@
+/* Status badges */
+.status-badge {
+ border-radius: 8px;
+ padding: 2px 8px;
+ font-size: 8pt;
+ font-weight: 600;
+}
+
+.status-badge.success {
+ background: @success_bg_color;
+ color: @success_fg_color;
+}
+
+.status-badge.warning {
+ background: @warning_bg_color;
+ color: @warning_fg_color;
+}
+
+.status-badge.error {
+ background: @error_bg_color;
+ color: @error_fg_color;
+}
+
+.status-badge.info {
+ background: @accent_bg_color;
+ color: @accent_fg_color;
+}
+
+.status-badge.neutral {
+ background: @card_shade_color;
+ color: @window_fg_color;
+}
+
+/* App cards in grid view */
+.app-card {
+ padding: 12px;
+ border-radius: 12px;
+ background: @card_bg_color;
+ transition: background 150ms ease;
+}
+
+.app-card:hover {
+ background: @headerbar_shade_color;
+}
+
+.app-card-name {
+ font-weight: 600;
+ font-size: 10pt;
+}
+
+.app-card-version {
+ font-size: 8pt;
+ opacity: 0.7;
+}
+
+/* Badge row in app cards */
+.badge-row {
+ margin-top: 4px;
+}
+
+/* Detail view headings */
+.heading {
+ font-weight: 700;
+ font-size: 11pt;
+ opacity: 0.8;
+}
+
+/* Monospace text for paths and hashes */
+.monospace {
+ font-family: monospace;
+ font-size: 9pt;
+}
diff --git a/packaging/build-appimage.sh b/packaging/build-appimage.sh
new file mode 100755
index 0000000..75a38f3
--- /dev/null
+++ b/packaging/build-appimage.sh
@@ -0,0 +1,110 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Build Driftwood as an AppImage using linuxdeploy
+#
+# Prerequisites:
+# - Rust toolchain (cargo)
+# - linuxdeploy (https://github.com/linuxdeploy/linuxdeploy)
+# - linuxdeploy-plugin-gtk (for GTK4/libadwaita bundling)
+#
+# Usage:
+# ./packaging/build-appimage.sh
+#
+# The resulting .AppImage will be in the project root.
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
+APP_ID="app.driftwood.Driftwood"
+
+cd "$PROJECT_DIR"
+
+echo "=== Building Driftwood (release) ==="
+cargo build --release
+
+echo "=== Preparing AppDir ==="
+APPDIR="$PROJECT_DIR/AppDir"
+rm -rf "$APPDIR"
+mkdir -p "$APPDIR/usr/bin"
+mkdir -p "$APPDIR/usr/share/applications"
+mkdir -p "$APPDIR/usr/share/glib-2.0/schemas"
+mkdir -p "$APPDIR/usr/share/icons/hicolor/scalable/apps"
+
+# Binary
+cp "target/release/driftwood" "$APPDIR/usr/bin/driftwood"
+
+# Desktop file
+cp "data/$APP_ID.desktop" "$APPDIR/usr/share/applications/$APP_ID.desktop"
+
+# GSettings schema
+cp "data/$APP_ID.gschema.xml" "$APPDIR/usr/share/glib-2.0/schemas/$APP_ID.gschema.xml"
+glib-compile-schemas "$APPDIR/usr/share/glib-2.0/schemas/"
+
+# Icon - use a placeholder SVG if no real icon exists yet
+ICON_FILE="data/icons/$APP_ID.svg"
+if [ -f "$ICON_FILE" ]; then
+ cp "$ICON_FILE" "$APPDIR/usr/share/icons/hicolor/scalable/apps/$APP_ID.svg"
+else
+ echo "Warning: No app icon found at $ICON_FILE"
+ echo "Creating a minimal placeholder icon..."
+ cat > "$APPDIR/usr/share/icons/hicolor/scalable/apps/$APP_ID.svg" << 'SVGEOF'
+
+SVGEOF
+fi
+
+# Check for linuxdeploy
+LINUXDEPLOY="${LINUXDEPLOY:-linuxdeploy}"
+if ! command -v "$LINUXDEPLOY" &>/dev/null; then
+ # Try to find it in the current directory
+ if [ -x "./linuxdeploy-x86_64.AppImage" ]; then
+ LINUXDEPLOY="./linuxdeploy-x86_64.AppImage"
+ else
+ echo ""
+ echo "Error: linuxdeploy not found."
+ echo ""
+ echo "Download it from:"
+ echo " https://github.com/linuxdeploy/linuxdeploy/releases"
+ echo ""
+ echo "Then either:"
+ echo " 1. Place linuxdeploy-x86_64.AppImage in the project root, or"
+ echo " 2. Set LINUXDEPLOY=/path/to/linuxdeploy"
+ exit 1
+ fi
+fi
+
+# Check for GTK plugin
+GTK_PLUGIN="${LINUXDEPLOY_PLUGIN_GTK:-}"
+if [ -z "$GTK_PLUGIN" ]; then
+ if [ -x "./linuxdeploy-plugin-gtk.sh" ]; then
+ export DEPLOY_GTK_VERSION=4
+ else
+ echo ""
+ echo "Warning: linuxdeploy-plugin-gtk not found."
+ echo "GTK4 libraries will not be bundled."
+ echo "The AppImage may only work on systems with GTK4 and libadwaita installed."
+ echo ""
+ echo "Download the plugin from:"
+ echo " https://github.com/nickvdp/linuxdeploy-plugin-gtk"
+ echo ""
+ fi
+fi
+
+echo "=== Building AppImage ==="
+export ARCH="x86_64"
+export OUTPUT="Driftwood-x86_64.AppImage"
+export GSETTINGS_SCHEMA_DIR="$APPDIR/usr/share/glib-2.0/schemas"
+
+"$LINUXDEPLOY" \
+ --appdir "$APPDIR" \
+ --desktop-file "$APPDIR/usr/share/applications/$APP_ID.desktop" \
+ --icon-file "$APPDIR/usr/share/icons/hicolor/scalable/apps/$APP_ID.svg" \
+ --output appimage
+
+echo ""
+echo "=== Done ==="
+echo "AppImage created: $OUTPUT"
+echo "You can run it with: ./$OUTPUT"
diff --git a/src/application.rs b/src/application.rs
new file mode 100644
index 0000000..7633696
--- /dev/null
+++ b/src/application.rs
@@ -0,0 +1,145 @@
+use adw::prelude::*;
+use adw::subclass::prelude::*;
+use gtk::gio;
+use std::cell::OnceCell;
+
+use crate::config::{APP_ID, VERSION};
+use crate::window::DriftwoodWindow;
+
+mod imp {
+ use super::*;
+
+ pub struct DriftwoodApplication {
+ pub settings: OnceCell,
+ }
+
+ impl Default for DriftwoodApplication {
+ fn default() -> Self {
+ Self {
+ settings: OnceCell::new(),
+ }
+ }
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for DriftwoodApplication {
+ const NAME: &'static str = "DriftwoodApplication";
+ type Type = super::DriftwoodApplication;
+ type ParentType = adw::Application;
+ }
+
+ impl ObjectImpl for DriftwoodApplication {}
+
+ impl ApplicationImpl for DriftwoodApplication {
+ fn startup(&self) {
+ self.parent_startup();
+ let app = self.obj();
+ app.setup_css();
+ app.setup_theme();
+ app.setup_actions();
+ }
+
+ fn activate(&self) {
+ self.parent_activate();
+ let app = self.obj();
+
+ // Present existing window or create a new one
+ if let Some(window) = app.active_window() {
+ window.present();
+ } else {
+ let window = DriftwoodWindow::new(&*app);
+ window.present();
+ }
+ }
+ }
+
+ impl GtkApplicationImpl for DriftwoodApplication {}
+ impl AdwApplicationImpl for DriftwoodApplication {}
+}
+
+glib::wrapper! {
+ pub struct DriftwoodApplication(ObjectSubclass)
+ @extends adw::Application, gtk::Application, gio::Application,
+ @implements gio::ActionGroup, gio::ActionMap;
+}
+
+impl DriftwoodApplication {
+ pub fn new(app_id: &str, flags: &gio::ApplicationFlags) -> Self {
+ glib::Object::builder()
+ .property("application-id", app_id)
+ .property("flags", flags)
+ .build()
+ }
+
+ fn setup_css(&self) {
+ let provider = gtk::CssProvider::new();
+ provider.load_from_resource("/app/driftwood/Driftwood/style.css");
+ gtk::style_context_add_provider_for_display(
+ >k::gdk::Display::default().expect("Could not get default display"),
+ &provider,
+ gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
+ );
+ }
+
+ fn setup_theme(&self) {
+ let settings = gio::Settings::new(APP_ID);
+ Self::apply_color_scheme(&settings);
+
+ settings.connect_changed(Some("color-scheme"), |settings, _| {
+ Self::apply_color_scheme(settings);
+ });
+
+ // Store settings on the imp struct so it stays alive
+ // (otherwise the connect_changed signal gets dropped)
+ self.imp()
+ .settings
+ .set(settings)
+ .expect("Theme settings already initialized");
+ }
+
+ fn apply_color_scheme(settings: &gio::Settings) {
+ let value = settings.string("color-scheme");
+ let scheme = match value.as_str() {
+ "force-light" => adw::ColorScheme::ForceLight,
+ "force-dark" => adw::ColorScheme::ForceDark,
+ _ => adw::ColorScheme::Default,
+ };
+ adw::StyleManager::default().set_color_scheme(scheme);
+ }
+
+ fn setup_actions(&self) {
+ // Quit action (Ctrl+Q)
+ let quit_action = gio::ActionEntry::builder("quit")
+ .activate(|app: &Self, _, _| {
+ if let Some(window) = app.active_window() {
+ window.close();
+ }
+ app.quit();
+ })
+ .build();
+
+ // About action
+ let about_action = gio::ActionEntry::builder("about")
+ .activate(|app: &Self, _, _| {
+ app.show_about_dialog();
+ })
+ .build();
+
+ self.add_action_entries([quit_action, about_action]);
+ self.set_accels_for_action("app.quit", &["q"]);
+ }
+
+ fn show_about_dialog(&self) {
+ let dialog = adw::AboutDialog::builder()
+ .application_name("Driftwood")
+ .application_icon(APP_ID)
+ .version(VERSION)
+ .developer_name("Driftwood Contributors")
+ .license_type(gtk::License::Gpl30)
+ .comments("A modern AppImage manager for GNOME desktops")
+ .website("https://github.com/driftwood-app/driftwood")
+ .build();
+
+ dialog.present(self.active_window().as_ref());
+ }
+}
diff --git a/src/cli.rs b/src/cli.rs
new file mode 100644
index 0000000..c5615c4
--- /dev/null
+++ b/src/cli.rs
@@ -0,0 +1,663 @@
+use clap::{Parser, Subcommand};
+use glib::ExitCode;
+use gtk::prelude::*;
+use std::time::Instant;
+
+use crate::core::database::Database;
+use crate::core::discovery;
+use crate::core::duplicates;
+use crate::core::fuse;
+use crate::core::inspector;
+use crate::core::integrator;
+use crate::core::launcher;
+use crate::core::orphan;
+use crate::core::updater;
+use crate::core::wayland;
+
+#[derive(Parser)]
+#[command(name = "driftwood", version, about = "Modern AppImage manager for GNOME desktops")]
+pub struct Cli {
+ #[command(subcommand)]
+ pub command: Option,
+}
+
+#[derive(Subcommand)]
+pub enum Commands {
+ /// List all known AppImages
+ List {
+ /// Output format: table or json
+ #[arg(long, default_value = "table")]
+ format: String,
+ },
+ /// Scan for AppImages in configured directories
+ Scan,
+ /// Integrate an AppImage (create .desktop and install icon)
+ Integrate {
+ /// Path to the AppImage
+ path: String,
+ },
+ /// Remove integration for an AppImage
+ Remove {
+ /// Path to the AppImage
+ path: String,
+ },
+ /// Clean orphaned desktop entries
+ Clean,
+ /// Inspect an AppImage and show its metadata
+ Inspect {
+ /// Path to the AppImage
+ path: String,
+ },
+ /// Show system status (FUSE, Wayland, desktop environment)
+ Status,
+ /// Check all AppImages for updates
+ CheckUpdates,
+ /// Find duplicate and multi-version AppImages
+ Duplicates,
+ /// Launch an AppImage (with tracking and FUSE detection)
+ Launch {
+ /// Path to the AppImage
+ path: String,
+ },
+}
+
+pub fn run_command(command: Commands) -> ExitCode {
+ let db = match Database::open() {
+ Ok(db) => db,
+ Err(e) => {
+ eprintln!("Error: Failed to open database: {}", e);
+ return ExitCode::FAILURE;
+ }
+ };
+
+ match command {
+ Commands::List { format } => cmd_list(&db, &format),
+ Commands::Scan => cmd_scan(&db),
+ Commands::Integrate { path } => cmd_integrate(&db, &path),
+ Commands::Remove { path } => cmd_remove(&db, &path),
+ Commands::Clean => cmd_clean(),
+ Commands::Inspect { path } => cmd_inspect(&path),
+ Commands::Status => cmd_status(),
+ Commands::CheckUpdates => cmd_check_updates(&db),
+ Commands::Duplicates => cmd_duplicates(&db),
+ Commands::Launch { path } => cmd_launch(&db, &path),
+ }
+}
+
+fn cmd_list(db: &Database, format: &str) -> ExitCode {
+ let records = match db.get_all_appimages() {
+ Ok(r) => r,
+ Err(e) => {
+ eprintln!("Error: {}", e);
+ return ExitCode::FAILURE;
+ }
+ };
+
+ if records.is_empty() {
+ println!("No AppImages found. Run 'driftwood scan' first.");
+ return ExitCode::SUCCESS;
+ }
+
+ if format == "json" {
+ // Simple JSON output
+ println!("[");
+ for (i, r) in records.iter().enumerate() {
+ let comma = if i + 1 < records.len() { "," } else { "" };
+ println!(
+ " {{\"name\": \"{}\", \"version\": \"{}\", \"path\": \"{}\", \"size\": {}, \"integrated\": {}}}{}",
+ r.app_name.as_deref().unwrap_or(&r.filename),
+ r.app_version.as_deref().unwrap_or(""),
+ r.path,
+ r.size_bytes,
+ r.integrated,
+ comma,
+ );
+ }
+ println!("]");
+ return ExitCode::SUCCESS;
+ }
+
+ // Table output
+ let name_width = records
+ .iter()
+ .map(|r| r.app_name.as_deref().unwrap_or(&r.filename).len())
+ .max()
+ .unwrap_or(4)
+ .max(4)
+ .min(30);
+
+ let ver_width = records
+ .iter()
+ .map(|r| r.app_version.as_deref().unwrap_or("").len())
+ .max()
+ .unwrap_or(7)
+ .max(7)
+ .min(15);
+
+ println!(
+ " {:10} {}",
+ "Name", "Version", "Size", "Integrated",
+ name_w = name_width,
+ ver_w = ver_width,
+ );
+ println!(
+ " {:-10} ----------",
+ "", "", "",
+ name_w = name_width,
+ ver_w = ver_width,
+ );
+
+ let mut integrated_count = 0;
+ for r in &records {
+ let name = r.app_name.as_deref().unwrap_or(&r.filename);
+ let display_name = if name.len() > name_width {
+ &name[..name_width]
+ } else {
+ name
+ };
+ let version = r.app_version.as_deref().unwrap_or("");
+ let size = humansize::format_size(r.size_bytes as u64, humansize::BINARY);
+ let status = if r.integrated { "Yes" } else { "No" };
+ if r.integrated {
+ integrated_count += 1;
+ }
+ println!(
+ " {:10} {}",
+ display_name, version, size, status,
+ name_w = name_width,
+ ver_w = ver_width,
+ );
+ }
+
+ println!();
+ println!(
+ " {} AppImages found, {} integrated",
+ records.len(),
+ integrated_count,
+ );
+
+ ExitCode::SUCCESS
+}
+
+fn cmd_scan(db: &Database) -> ExitCode {
+ let settings = gtk::gio::Settings::new(crate::config::APP_ID);
+ let dirs: Vec = settings
+ .strv("scan-directories")
+ .iter()
+ .map(|s| s.to_string())
+ .collect();
+
+ println!("Scanning directories:");
+ for d in &dirs {
+ let expanded = discovery::expand_tilde(d);
+ println!(" {}", expanded.display());
+ }
+
+ let start = Instant::now();
+ let discovered = discovery::scan_directories(&dirs);
+ let total = discovered.len();
+ let mut new_count = 0;
+
+ for d in &discovered {
+ let existing = db
+ .get_appimage_by_path(&d.path.to_string_lossy())
+ .ok()
+ .flatten();
+
+ let modified = d.modified_time
+ .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
+ .and_then(|dur| {
+ chrono::DateTime::from_timestamp(dur.as_secs() as i64, 0)
+ .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
+ });
+
+ let id = db.upsert_appimage(
+ &d.path.to_string_lossy(),
+ &d.filename,
+ Some(d.appimage_type.as_i32()),
+ d.size_bytes as i64,
+ d.is_executable,
+ modified.as_deref(),
+ ).unwrap_or(0);
+
+ if existing.is_none() {
+ new_count += 1;
+ println!(" [NEW] {}", d.filename);
+ }
+
+ let needs_metadata = existing
+ .as_ref()
+ .map(|r| r.app_name.is_none())
+ .unwrap_or(true);
+
+ if needs_metadata {
+ print!(" Inspecting {}... ", d.filename);
+ match inspector::inspect_appimage(&d.path, &d.appimage_type) {
+ Ok(metadata) => {
+ let categories = if metadata.categories.is_empty() {
+ None
+ } else {
+ Some(metadata.categories.join(";"))
+ };
+ db.update_metadata(
+ id,
+ metadata.app_name.as_deref(),
+ metadata.app_version.as_deref(),
+ metadata.description.as_deref(),
+ metadata.developer.as_deref(),
+ categories.as_deref(),
+ metadata.architecture.as_deref(),
+ metadata.cached_icon_path.as_ref().map(|p| p.to_string_lossy()).as_deref(),
+ Some(&metadata.desktop_entry_content),
+ ).ok();
+ println!(
+ "{}",
+ metadata.app_name.as_deref().unwrap_or("(no name)")
+ );
+ }
+ Err(e) => {
+ println!("failed: {}", e);
+ }
+ }
+ }
+ }
+
+ let duration = start.elapsed();
+ db.log_scan(
+ "cli",
+ &dirs,
+ total as i32,
+ new_count,
+ 0,
+ duration.as_millis() as i64,
+ ).ok();
+
+ println!();
+ println!(
+ "Scan complete: {} found, {} new ({:.1}s)",
+ total,
+ new_count,
+ duration.as_secs_f64(),
+ );
+
+ ExitCode::SUCCESS
+}
+
+fn cmd_integrate(db: &Database, path: &str) -> ExitCode {
+ let record = match db.get_appimage_by_path(path) {
+ Ok(Some(r)) => r,
+ Ok(None) => {
+ eprintln!("Error: '{}' is not in the database. Run 'driftwood scan' first.", path);
+ return ExitCode::FAILURE;
+ }
+ Err(e) => {
+ eprintln!("Error: {}", e);
+ return ExitCode::FAILURE;
+ }
+ };
+
+ if record.integrated {
+ println!("{} is already integrated.", record.app_name.as_deref().unwrap_or(&record.filename));
+ return ExitCode::SUCCESS;
+ }
+
+ match integrator::integrate(&record) {
+ Ok(result) => {
+ db.set_integrated(
+ record.id,
+ true,
+ Some(&result.desktop_file_path.to_string_lossy()),
+ ).ok();
+ println!(
+ "Integrated {} -> {}",
+ record.app_name.as_deref().unwrap_or(&record.filename),
+ result.desktop_file_path.display(),
+ );
+ ExitCode::SUCCESS
+ }
+ Err(e) => {
+ eprintln!("Error: {}", e);
+ ExitCode::FAILURE
+ }
+ }
+}
+
+fn cmd_remove(db: &Database, path: &str) -> ExitCode {
+ let record = match db.get_appimage_by_path(path) {
+ Ok(Some(r)) => r,
+ Ok(None) => {
+ eprintln!("Error: '{}' is not in the database.", path);
+ return ExitCode::FAILURE;
+ }
+ Err(e) => {
+ eprintln!("Error: {}", e);
+ return ExitCode::FAILURE;
+ }
+ };
+
+ if !record.integrated {
+ println!("{} is not integrated.", record.app_name.as_deref().unwrap_or(&record.filename));
+ return ExitCode::SUCCESS;
+ }
+
+ match integrator::remove_integration(&record) {
+ Ok(()) => {
+ db.set_integrated(record.id, false, None).ok();
+ println!(
+ "Removed integration for {}",
+ record.app_name.as_deref().unwrap_or(&record.filename),
+ );
+ ExitCode::SUCCESS
+ }
+ Err(e) => {
+ eprintln!("Error: {}", e);
+ ExitCode::FAILURE
+ }
+ }
+}
+
+fn cmd_clean() -> ExitCode {
+ let orphans = orphan::detect_orphans();
+
+ if orphans.is_empty() {
+ println!("No orphaned desktop entries found.");
+ return ExitCode::SUCCESS;
+ }
+
+ println!("Found {} orphaned entries:", orphans.len());
+ for entry in &orphans {
+ println!(
+ " {} (was: {})",
+ entry.app_name.as_deref().unwrap_or("Unknown"),
+ entry.original_appimage_path,
+ );
+ }
+
+ match orphan::clean_all_orphans() {
+ Ok(summary) => {
+ println!(
+ "Cleaned {} desktop entries, {} icons",
+ summary.entries_removed,
+ summary.icons_removed,
+ );
+ ExitCode::SUCCESS
+ }
+ Err(e) => {
+ eprintln!("Error during cleanup: {}", e);
+ ExitCode::FAILURE
+ }
+ }
+}
+
+fn cmd_inspect(path: &str) -> ExitCode {
+ let file_path = std::path::Path::new(path);
+ if !file_path.exists() {
+ eprintln!("Error: file not found: {}", path);
+ return ExitCode::FAILURE;
+ }
+
+ // Detect AppImage type
+ let discovered = discovery::scan_directories(&[path.to_string()]);
+ if discovered.is_empty() {
+ // Try scanning the parent directory and finding by path
+ let parent = file_path.parent().unwrap_or(std::path::Path::new("."));
+ let all = discovery::scan_directories(&[parent.to_string_lossy().to_string()]);
+ let found = all.iter().find(|d| d.path == file_path);
+ if let Some(d) = found {
+ return do_inspect(file_path, &d.appimage_type);
+ }
+ eprintln!("Error: '{}' does not appear to be an AppImage", path);
+ return ExitCode::FAILURE;
+ }
+
+ let d = &discovered[0];
+ do_inspect(file_path, &d.appimage_type)
+}
+
+fn cmd_status() -> ExitCode {
+ println!("System Status");
+ println!("=============");
+ println!();
+
+ // Display server
+ let session = wayland::detect_session_type();
+ println!(" Display server: {}", session.label());
+
+ // Desktop environment
+ let de = wayland::detect_desktop_environment();
+ println!(" Desktop: {}", de);
+
+ // XWayland
+ println!(
+ " XWayland: {}",
+ if wayland::has_xwayland() {
+ "running"
+ } else {
+ "not detected"
+ }
+ );
+ println!();
+
+ // FUSE
+ let fuse_info = fuse::detect_system_fuse();
+ println!(" FUSE status: {}", fuse_info.status.label());
+ println!(" libfuse2: {}", if fuse_info.has_libfuse2 { "yes" } else { "no" });
+ println!(" libfuse3: {}", if fuse_info.has_libfuse3 { "yes" } else { "no" });
+ println!(" fusermount: {}", fuse_info.fusermount_path.as_deref().unwrap_or("not found"));
+ println!(" /dev/fuse: {}", if fuse_info.has_dev_fuse { "present" } else { "missing" });
+
+ if let Some(ref hint) = fuse_info.install_hint {
+ println!();
+ println!(" Fix: {}", hint);
+ }
+
+ // AppImageLauncher
+ if let Some(version) = fuse::detect_appimagelauncher() {
+ println!();
+ println!(" WARNING: AppImageLauncher v{} detected (may conflict)", version);
+ }
+
+ println!();
+
+ // AppImageUpdate tool
+ println!(
+ " AppImageUpdate: {}",
+ if updater::has_appimage_update_tool() {
+ "available (delta updates enabled)"
+ } else {
+ "not found (full downloads only)"
+ }
+ );
+
+ ExitCode::SUCCESS
+}
+
+fn cmd_check_updates(db: &Database) -> ExitCode {
+ let records = match db.get_all_appimages() {
+ Ok(r) => r,
+ Err(e) => {
+ eprintln!("Error: {}", e);
+ return ExitCode::FAILURE;
+ }
+ };
+
+ if records.is_empty() {
+ println!("No AppImages found. Run 'driftwood scan' first.");
+ return ExitCode::SUCCESS;
+ }
+
+ println!("Checking {} AppImages for updates...", records.len());
+ println!();
+
+ let mut updates_found = 0;
+
+ for record in &records {
+ let name = record.app_name.as_deref().unwrap_or(&record.filename);
+ let appimage_path = std::path::Path::new(&record.path);
+
+ if !appimage_path.exists() {
+ continue;
+ }
+
+ print!(" {} ... ", name);
+
+ let (type_label, raw_info, check_result) = updater::check_appimage_for_update(
+ appimage_path,
+ record.app_version.as_deref(),
+ );
+
+ // Store update info
+ if raw_info.is_some() || type_label.is_some() {
+ db.update_update_info(record.id, raw_info.as_deref(), type_label.as_deref()).ok();
+ }
+
+ match check_result {
+ Some(result) if result.update_available => {
+ let latest = result.latest_version.as_deref().unwrap_or("unknown");
+ println!(
+ "UPDATE AVAILABLE ({} -> {})",
+ record.app_version.as_deref().unwrap_or("?"),
+ latest,
+ );
+ db.set_update_available(record.id, Some(latest), result.download_url.as_deref()).ok();
+ updates_found += 1;
+ }
+ Some(_) => {
+ println!("up to date");
+ db.clear_update_available(record.id).ok();
+ }
+ None => {
+ if raw_info.is_none() {
+ println!("no update info");
+ } else {
+ println!("check failed");
+ }
+ }
+ }
+ }
+
+ println!();
+ if updates_found == 0 {
+ println!("All AppImages are up to date.");
+ } else {
+ println!("{} update{} available.", updates_found, if updates_found == 1 { "" } else { "s" });
+ }
+
+ ExitCode::SUCCESS
+}
+
+fn cmd_duplicates(db: &Database) -> ExitCode {
+ let groups = duplicates::detect_duplicates(db);
+
+ if groups.is_empty() {
+ println!("No duplicate or multi-version AppImages found.");
+ return ExitCode::SUCCESS;
+ }
+
+ let summary = duplicates::summarize_duplicates(&groups);
+ println!(
+ "Found {} duplicate groups ({} exact, {} multi-version)",
+ summary.total_groups,
+ summary.exact_duplicates,
+ summary.multi_version,
+ );
+ println!(
+ "Potential savings: {}",
+ humansize::format_size(summary.total_potential_savings, humansize::BINARY),
+ );
+ println!();
+
+ for group in &groups {
+ println!(" {} ({})", group.app_name, group.match_reason.label());
+ for member in &group.members {
+ let r = &member.record;
+ let version = r.app_version.as_deref().unwrap_or("?");
+ let size = humansize::format_size(r.size_bytes as u64, humansize::BINARY);
+ let rec = member.recommendation.label();
+ println!(" {} v{} ({}) - {}", r.path, version, size, rec);
+ }
+ if group.potential_savings > 0 {
+ println!(
+ " Savings: {}",
+ humansize::format_size(group.potential_savings, humansize::BINARY),
+ );
+ }
+ println!();
+ }
+
+ ExitCode::SUCCESS
+}
+
+fn cmd_launch(db: &Database, path: &str) -> ExitCode {
+ let file_path = std::path::Path::new(path);
+ if !file_path.exists() {
+ eprintln!("Error: file not found: {}", path);
+ return ExitCode::FAILURE;
+ }
+
+ // Try to find in database for tracking
+ let record = db.get_appimage_by_path(path).ok().flatten();
+
+ if let Some(ref record) = record {
+ match launcher::launch_appimage(db, record.id, file_path, "cli", &[], &[]) {
+ launcher::LaunchResult::Started { method, .. } => {
+ println!(
+ "Launched {} ({})",
+ record.app_name.as_deref().unwrap_or(&record.filename),
+ method.as_str(),
+ );
+ ExitCode::SUCCESS
+ }
+ launcher::LaunchResult::Failed(msg) => {
+ eprintln!("Error: {}", msg);
+ ExitCode::FAILURE
+ }
+ }
+ } else {
+ // Not in database - launch without tracking
+ match launcher::launch_appimage_simple(file_path, &[]) {
+ launcher::LaunchResult::Started { method, .. } => {
+ println!("Launched {} ({})", path, method.as_str());
+ ExitCode::SUCCESS
+ }
+ launcher::LaunchResult::Failed(msg) => {
+ eprintln!("Error: {}", msg);
+ ExitCode::FAILURE
+ }
+ }
+ }
+}
+
+fn do_inspect(path: &std::path::Path, appimage_type: &discovery::AppImageType) -> ExitCode {
+ println!("Inspecting: {}", path.display());
+ println!("Type: {:?}", appimage_type);
+
+ match inspector::inspect_appimage(path, appimage_type) {
+ Ok(metadata) => {
+ println!("Name: {}", metadata.app_name.as_deref().unwrap_or("(unknown)"));
+ println!("Version: {}", metadata.app_version.as_deref().unwrap_or("(unknown)"));
+ if let Some(ref desc) = metadata.description {
+ println!("Description: {}", desc);
+ }
+ if let Some(ref arch) = metadata.architecture {
+ println!("Architecture: {}", arch);
+ }
+ if !metadata.categories.is_empty() {
+ println!("Categories: {}", metadata.categories.join(", "));
+ }
+ if let Some(ref icon) = metadata.cached_icon_path {
+ println!("Icon: {}", icon.display());
+ }
+ if !metadata.desktop_entry_content.is_empty() {
+ println!();
+ println!("--- Desktop Entry ---");
+ println!("{}", metadata.desktop_entry_content);
+ }
+ ExitCode::SUCCESS
+ }
+ Err(e) => {
+ eprintln!("Inspection failed: {}", e);
+ ExitCode::FAILURE
+ }
+ }
+}
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..cd5bfcd
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,3 @@
+pub const APP_ID: &str = "app.driftwood.Driftwood";
+pub const VERSION: &str = env!("CARGO_PKG_VERSION");
+pub const GSETTINGS_SCHEMA_DIR: &str = env!("GSETTINGS_SCHEMA_DIR");
diff --git a/src/core/database.rs b/src/core/database.rs
new file mode 100644
index 0000000..8c94435
--- /dev/null
+++ b/src/core/database.rs
@@ -0,0 +1,777 @@
+use rusqlite::{params, Connection, Result as SqlResult};
+use std::path::PathBuf;
+
+pub struct Database {
+ conn: Connection,
+}
+
+#[derive(Debug, Clone)]
+pub struct AppImageRecord {
+ pub id: i64,
+ pub path: String,
+ pub filename: String,
+ pub app_name: Option,
+ pub app_version: Option,
+ pub appimage_type: Option,
+ pub size_bytes: i64,
+ pub sha256: Option,
+ pub icon_path: Option,
+ pub desktop_file: Option,
+ pub integrated: bool,
+ pub integrated_at: Option,
+ pub is_executable: bool,
+ pub desktop_entry_content: Option,
+ pub categories: Option,
+ pub description: Option,
+ pub developer: Option,
+ pub architecture: Option,
+ pub first_seen: String,
+ pub last_scanned: String,
+ pub file_modified: Option,
+ // Phase 2 fields
+ pub fuse_status: Option,
+ pub wayland_status: Option,
+ pub update_info: Option,
+ pub update_type: Option,
+ pub latest_version: Option,
+ pub update_checked: Option,
+ pub update_url: Option,
+ pub notes: Option,
+}
+
+#[derive(Debug, Clone)]
+pub struct OrphanedEntry {
+ pub id: i64,
+ pub desktop_file: String,
+ pub original_path: Option,
+ pub app_name: Option,
+ pub detected_at: String,
+ pub cleaned: bool,
+}
+
+#[derive(Debug, Clone)]
+pub struct LaunchEvent {
+ pub id: i64,
+ pub appimage_id: i64,
+ pub launched_at: String,
+ pub source: String,
+}
+
+#[derive(Debug, Clone)]
+pub struct UpdateHistoryEntry {
+ pub id: i64,
+ pub appimage_id: i64,
+ pub from_version: Option,
+ pub to_version: Option,
+ pub update_method: Option,
+ pub download_size: Option,
+ pub updated_at: String,
+ pub success: bool,
+}
+
+fn db_path() -> PathBuf {
+ let data_dir = dirs::data_dir()
+ .unwrap_or_else(|| PathBuf::from("~/.local/share"))
+ .join("driftwood");
+ std::fs::create_dir_all(&data_dir).ok();
+ data_dir.join("driftwood.db")
+}
+
+impl Database {
+ pub fn open() -> SqlResult {
+ let path = db_path();
+ let conn = Connection::open(&path)?;
+ let db = Self { conn };
+ db.init_schema()?;
+ Ok(db)
+ }
+
+ pub fn open_in_memory() -> SqlResult {
+ let conn = Connection::open_in_memory()?;
+ let db = Self { conn };
+ db.init_schema()?;
+ Ok(db)
+ }
+
+ fn init_schema(&self) -> SqlResult<()> {
+ // Phase 1 base tables
+ self.conn.execute_batch(
+ "CREATE TABLE IF NOT EXISTS schema_version (
+ version INTEGER NOT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS appimages (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ path TEXT NOT NULL UNIQUE,
+ filename TEXT NOT NULL,
+ app_name TEXT,
+ app_version TEXT,
+ appimage_type INTEGER,
+ size_bytes INTEGER NOT NULL DEFAULT 0,
+ sha256 TEXT,
+ icon_path TEXT,
+ desktop_file TEXT,
+ integrated INTEGER NOT NULL DEFAULT 0,
+ integrated_at TEXT,
+ is_executable INTEGER NOT NULL DEFAULT 0,
+ desktop_entry_content TEXT,
+ categories TEXT,
+ description TEXT,
+ developer TEXT,
+ architecture TEXT,
+ first_seen TEXT NOT NULL DEFAULT (datetime('now')),
+ last_scanned TEXT NOT NULL DEFAULT (datetime('now')),
+ file_modified TEXT
+ );
+
+ CREATE TABLE IF NOT EXISTS orphaned_entries (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ desktop_file TEXT NOT NULL,
+ original_path TEXT,
+ app_name TEXT,
+ detected_at TEXT NOT NULL DEFAULT (datetime('now')),
+ cleaned INTEGER NOT NULL DEFAULT 0
+ );
+
+ CREATE TABLE IF NOT EXISTS scan_log (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ scan_type TEXT NOT NULL,
+ directories TEXT,
+ found INTEGER NOT NULL DEFAULT 0,
+ new_count INTEGER NOT NULL DEFAULT 0,
+ removed INTEGER NOT NULL DEFAULT 0,
+ duration_ms INTEGER NOT NULL DEFAULT 0,
+ scanned_at TEXT NOT NULL DEFAULT (datetime('now'))
+ );"
+ )?;
+
+ // Check current schema version and migrate
+ let count: i32 = self.conn.query_row(
+ "SELECT COUNT(*) FROM schema_version",
+ [],
+ |row| row.get(0),
+ )?;
+
+ let current_version = if count == 0 {
+ self.conn.execute(
+ "INSERT INTO schema_version (version) VALUES (?1)",
+ params![1],
+ )?;
+ 1
+ } else {
+ self.conn.query_row(
+ "SELECT version FROM schema_version LIMIT 1",
+ [],
+ |row| row.get::<_, i32>(0),
+ )?
+ };
+
+ if current_version < 2 {
+ self.migrate_to_v2()?;
+ }
+
+ Ok(())
+ }
+
+ fn migrate_to_v2(&self) -> SqlResult<()> {
+ // Add Phase 2 columns to appimages table
+ let phase2_columns = [
+ "fuse_status TEXT",
+ "wayland_status TEXT",
+ "update_info TEXT",
+ "update_type TEXT",
+ "latest_version TEXT",
+ "update_checked TEXT",
+ "update_url TEXT",
+ "notes TEXT",
+ ];
+ for col in &phase2_columns {
+ let sql = format!("ALTER TABLE appimages ADD COLUMN {}", col);
+ // Ignore errors from columns that already exist
+ self.conn.execute_batch(&sql).ok();
+ }
+
+ // Phase 2 tables
+ self.conn.execute_batch(
+ "CREATE TABLE IF NOT EXISTS launch_events (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ appimage_id INTEGER REFERENCES appimages(id) ON DELETE CASCADE,
+ launched_at TEXT NOT NULL DEFAULT (datetime('now')),
+ source TEXT NOT NULL DEFAULT 'desktop_entry'
+ );
+
+ CREATE TABLE IF NOT EXISTS update_history (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ appimage_id INTEGER REFERENCES appimages(id) ON DELETE CASCADE,
+ from_version TEXT,
+ to_version TEXT,
+ update_method TEXT,
+ download_size INTEGER,
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
+ success INTEGER NOT NULL DEFAULT 0
+ );
+
+ CREATE TABLE IF NOT EXISTS duplicate_groups (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ canonical_name TEXT NOT NULL,
+ duplicate_type TEXT,
+ detected_at TEXT NOT NULL DEFAULT (datetime('now'))
+ );
+
+ CREATE TABLE IF NOT EXISTS duplicate_members (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ group_id INTEGER REFERENCES duplicate_groups(id) ON DELETE CASCADE,
+ appimage_id INTEGER REFERENCES appimages(id) ON DELETE CASCADE,
+ is_recommended INTEGER NOT NULL DEFAULT 0
+ );"
+ )?;
+
+ // Update schema version
+ self.conn.execute(
+ "UPDATE schema_version SET version = ?1",
+ params![2],
+ )?;
+
+ Ok(())
+ }
+
+ pub fn upsert_appimage(
+ &self,
+ path: &str,
+ filename: &str,
+ appimage_type: Option,
+ size_bytes: i64,
+ is_executable: bool,
+ file_modified: Option<&str>,
+ ) -> SqlResult {
+ self.conn.execute(
+ "INSERT INTO appimages (path, filename, appimage_type, size_bytes, is_executable, file_modified)
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6)
+ ON CONFLICT(path) DO UPDATE SET
+ filename = excluded.filename,
+ appimage_type = excluded.appimage_type,
+ size_bytes = excluded.size_bytes,
+ is_executable = excluded.is_executable,
+ file_modified = excluded.file_modified,
+ last_scanned = datetime('now')",
+ params![path, filename, appimage_type, size_bytes, is_executable, file_modified],
+ )?;
+ Ok(self.conn.last_insert_rowid())
+ }
+
+ pub fn update_metadata(
+ &self,
+ id: i64,
+ app_name: Option<&str>,
+ app_version: Option<&str>,
+ description: Option<&str>,
+ developer: Option<&str>,
+ categories: Option<&str>,
+ architecture: Option<&str>,
+ icon_path: Option<&str>,
+ desktop_entry_content: Option<&str>,
+ ) -> SqlResult<()> {
+ self.conn.execute(
+ "UPDATE appimages SET
+ app_name = ?2,
+ app_version = ?3,
+ description = ?4,
+ developer = ?5,
+ categories = ?6,
+ architecture = ?7,
+ icon_path = ?8,
+ desktop_entry_content = ?9
+ WHERE id = ?1",
+ params![
+ id, app_name, app_version, description, developer,
+ categories, architecture, icon_path, desktop_entry_content,
+ ],
+ )?;
+ Ok(())
+ }
+
+ pub fn update_sha256(&self, id: i64, sha256: &str) -> SqlResult<()> {
+ self.conn.execute(
+ "UPDATE appimages SET sha256 = ?2 WHERE id = ?1",
+ params![id, sha256],
+ )?;
+ Ok(())
+ }
+
+ pub fn set_integrated(
+ &self,
+ id: i64,
+ integrated: bool,
+ desktop_file: Option<&str>,
+ ) -> SqlResult<()> {
+ let integrated_at = if integrated {
+ Some(chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string())
+ } else {
+ None
+ };
+ self.conn.execute(
+ "UPDATE appimages SET integrated = ?2, desktop_file = ?3, integrated_at = ?4 WHERE id = ?1",
+ params![id, integrated, desktop_file, integrated_at],
+ )?;
+ Ok(())
+ }
+
+ const APPIMAGE_COLUMNS: &str =
+ "id, path, filename, app_name, app_version, appimage_type,
+ size_bytes, sha256, icon_path, desktop_file, integrated,
+ integrated_at, is_executable, desktop_entry_content,
+ categories, description, developer, architecture,
+ first_seen, last_scanned, file_modified,
+ fuse_status, wayland_status, update_info, update_type,
+ latest_version, update_checked, update_url, notes";
+
+ fn row_to_record(row: &rusqlite::Row) -> rusqlite::Result {
+ Ok(AppImageRecord {
+ id: row.get(0)?,
+ path: row.get(1)?,
+ filename: row.get(2)?,
+ app_name: row.get(3)?,
+ app_version: row.get(4)?,
+ appimage_type: row.get(5)?,
+ size_bytes: row.get(6)?,
+ sha256: row.get(7)?,
+ icon_path: row.get(8)?,
+ desktop_file: row.get(9)?,
+ integrated: row.get(10)?,
+ integrated_at: row.get(11)?,
+ is_executable: row.get(12)?,
+ desktop_entry_content: row.get(13)?,
+ categories: row.get(14)?,
+ description: row.get(15)?,
+ developer: row.get(16)?,
+ architecture: row.get(17)?,
+ first_seen: row.get(18)?,
+ last_scanned: row.get(19)?,
+ file_modified: row.get(20)?,
+ fuse_status: row.get(21)?,
+ wayland_status: row.get(22)?,
+ update_info: row.get(23)?,
+ update_type: row.get(24)?,
+ latest_version: row.get(25)?,
+ update_checked: row.get(26)?,
+ update_url: row.get(27)?,
+ notes: row.get(28)?,
+ })
+ }
+
+ pub fn get_all_appimages(&self) -> SqlResult> {
+ let sql = format!(
+ "SELECT {} FROM appimages ORDER BY app_name COLLATE NOCASE, filename",
+ Self::APPIMAGE_COLUMNS
+ );
+ let mut stmt = self.conn.prepare(&sql)?;
+ let rows = stmt.query_map([], Self::row_to_record)?;
+ rows.collect()
+ }
+
+ pub fn get_appimage_by_id(&self, id: i64) -> SqlResult