From c3f8ba4cb6952bb6bfcf92cce6f06918b61d8727 Mon Sep 17 00:00:00 2001 From: Trude Date: Mon, 9 Mar 2026 16:09:28 +0000 Subject: [PATCH] First commit --- .gitignore | 1 + Cargo.lock | 2397 +++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 13 + README.md | 150 +++ src/main.rs | 990 +++++++++++++++++++ src/projection.rs | 223 +++++ src/store.rs | 764 +++++++++++++++ 7 files changed, 4538 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/main.rs create mode 100644 src/projection.rs create mode 100644 src/store.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..67c86f6 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2397 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[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 = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "ashpd" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand", + "raw-window-handle", + "serde", + "serde_repr", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "cairo-rs" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b01fe135c0bd16afe262b6dea349bd5ea30e6de50708cec639aae7c5c14cc7e4" +dependencies = [ + "bitflags", + "cairo-sys-rs", + "glib", + "libc", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06c28280c6b12055b5e39e4554271ae4e6630b27c0da9148c4cf6485fc6d245c" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" +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", + "num-traits", + "windows-link", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + +[[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 = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[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 = "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-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[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-io", + "futures-macro", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debb0d39e3cdd84626edfd54d6e4a6ba2da9a0ef2e796e691c4e9f8646fda00c" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd95ad50b9a3d2551e25dd4f6892aff0b772fe5372d84514e9d0583af60a0ce7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk4" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756564212bbe4a4ce05d88ffbd2582581ac6003832d0d32822d0825cca84bfbf" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk4-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk4-sys" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6d4e5b3ccf591826a4adcc83f5f57b4e59d1925cb4bf620b0d645f79498b034" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gio" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5ff48bf600c68b476e61dc6b7c762f2f4eb91deef66583ba8bb815c30b5811a" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "pin-project-lite", + "smallvec", +] + +[[package]] +name = "gio-sys" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0071fe88dba8e40086c8ff9bbb62622999f49628344b1d1bf490a48a29d80f22" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "windows-sys 0.61.2", +] + +[[package]] +name = "glib" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16de123c2e6c90ce3b573b7330de19be649080ec612033d397d72da265f1bd8b" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "smallvec", +] + +[[package]] +name = "glib-macros" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf59b675301228a696fe01c3073974643365080a76cc3ed5bc2cbc466ad87f17" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "glib-sys" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d95e1a3a19ae464a7286e14af9a90683c64d70c02532d88d87ce95056af3e6c" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "gobject-sys" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dca35da0d19a18f4575f3cb99fe1c9e029a2941af5662f326f738a21edaf294" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "graphene-rs" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2730030ac9db663fd8bfe1e7093742c1cafb92db9c315c9417c29032341fe2f9" +dependencies = [ + "glib", + "graphene-sys", + "libc", +] + +[[package]] +name = "graphene-sys" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915e32091ea9ad241e4b044af62b7351c2d68aeb24f489a0d7f37a0fc484fd93" +dependencies = [ + "glib-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gsk4" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e755de9d8c5896c5beaa028b89e1969d067f1b9bf1511384ede971f5983aa153" +dependencies = [ + "cairo-rs", + "gdk4", + "glib", + "graphene-rs", + "gsk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gsk4-sys" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce91472391146f482065f1041876d8f869057b195b95399414caa163d72f4f7" +dependencies = [ + "cairo-sys-rs", + "gdk4-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk4" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acb21d53cfc6f7bfaf43549731c43b67ca47d87348d81c8cfc4dcdd44828e1a4" +dependencies = [ + "cairo-rs", + "field-offset", + "futures-channel", + "gdk-pixbuf", + "gdk4", + "gio", + "glib", + "graphene-rs", + "gsk4", + "gtk4-macros", + "gtk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gtk4-macros" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ccfb5a14a3d941244815d5f8101fa12d4577b59cc47245778d8d907b0003e42" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "gtk4-sys" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "842577fe5a1ee15d166cd3afe804ce0cab6173bc789ca32e21308834f20088dd" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "gsk4-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "hashbrown" +version = "0.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 = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[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 = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +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 = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +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 = "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 = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "pango" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1d85e2078077a065bb7fc072783d5bcd4e51b379f22d67107d0a16937eb69" +dependencies = [ + "gio", + "glib", + "libc", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4f06627d36ed5ff303d2df65211fc2e52ba5b17bf18dd80ff3d9628d6e06cfd" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[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.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[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.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +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 = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rfd" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +dependencies = [ + "ashpd", + "block2", + "dispatch2", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "pollster", + "raw-window-handle", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rusqlite" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +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 = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[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_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[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 = "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.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[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 0.7.5+spec-1.1.0", + "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_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" +dependencies = [ + "indexmap", + "toml_datetime 1.0.0+spec-1.1.0", + "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 = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "uds_windows" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b70b87d15e91f553711b40df3048faf27a7a04e01e0ddc0cf9309f0af7c2ca" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[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 = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vector_explorer" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "gtk4", + "rfd", + "rusqlite", + "serde", + "serde_json", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[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.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +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 = "wayland-backend" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa75f400b7f719bcd68b3f47cd939ba654cedeef690f486db71331eec4c6a406" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab51d9f7c071abeee76007e2b742499e535148035bb835f97aaed1338cf516c3" +dependencies = [ + "bitflags", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23b5df31ceff1328f06ac607591d5ba360cf58f90c8fad4ac8d3a55a3c4aec7" +dependencies = [ + "bitflags", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86287151a309799b821ca709b7345a048a2956af05957c89cb824ab919fa4e3" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374f6b70e8e0d6bf9461a32988fd553b59ff630964924dad6e4a4eb6bd538d17" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[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.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +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.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +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 = "zbus" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +dependencies = [ + "serde", + "winnow", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "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" + +[[package]] +name = "zvariant" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" +dependencies = [ + "endi", + "enumflags2", + "serde", + "url", + "winnow", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn", + "winnow", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..86adff3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "vector_explorer" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0" +chrono = { version = "0.4", default-features = false, features = ["clock"] } +gtk4 = "0.10" +rfd = "0.15" +rusqlite = { version = "0.37", features = ["bundled"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..bdd90ca --- /dev/null +++ b/README.md @@ -0,0 +1,150 @@ +# Vector Explorer + +`vector_explorer` is a Rust + GTK desktop viewer for SQLite-backed vector stores. It is designed around OpenClaw's memory layout and can fall back to a generic adapter for similar databases such as NullClaw- or ZeroClaw-style stores that keep chunk text and embeddings in SQLite tables. + +## Features + +- Store overview with detected adapter, file count, chunk count, embedding dimensions, models, and inferred backend features such as FTS5 or `sqlite-vec` +- Filterable file list with a top-level reset entry to return to the full graph +- Rotatable 3D semantic map based on a lightweight PCA-style projection of embeddings +- Detail panel for the selected chunk or file +- OpenClaw-first schema detection with generic SQLite heuristics for other stores + +## Supported store layouts + +### OpenClaw + +The first-class adapter targets the documented OpenClaw memory structure: + +- `files` for indexed documents +- `chunks` for chunk text and stored embeddings +- `meta` for memory index metadata +- `chunks_fts` and `chunks_vec` as optional hybrid-search acceleration tables + +The sample `main.sqlite` in this repository matches that layout. + +### Generic SQLite vector stores + +If the database is not recognized as OpenClaw, the app falls back to schema heuristics: + +- Find a table with text/content columns +- Prefer tables that also expose `embedding` or `vector` +- Reconstruct file groupings from a path-like column or from the chunk table itself + +That makes the viewer usable for adjacent SQLite-backed vector stores such as NullClaw- or ZeroClaw-style databases, as long as they expose chunk text and embeddings in a reasonably conventional schema. + +## Rust dependencies + +These are declared in [Cargo.toml](C:/Users/Trude/Desktop/vector_explorer/Cargo.toml): + +- `anyhow` +- `chrono` +- `gtk4` +- `rfd` +- `rusqlite` with `bundled` +- `serde` +- `serde_json` + +## System dependencies + +### Required + +- Rust toolchain with `cargo` +- GTK 4 development files and runtime +- `pkg-config` +- C/C++ build tooling appropriate for your platform + +### WSL / Ubuntu packages + +If you are building in WSL on an Ubuntu-like distro, install: + +```bash +sudo apt update +sudo apt install -y build-essential pkg-config libgtk-4-dev +``` + +### Windows native build + +If you are building natively on Windows instead of WSL, you need: + +- Visual Studio Build Tools with the MSVC C++ toolchain +- GTK 4 SDK/runtime +- `pkg-config` configured to find the GTK installation + +The project compiles more reliably in WSL than in a partially configured native Windows environment. + +## How to compile + +### WSL + +```bash +cd /mnt/c/Users/Trude/Desktop/vector_explorer +cargo build +``` + +For an optimized build: + +```bash +cd /mnt/c/Users/Trude/Desktop/vector_explorer +cargo build --release +``` + +### Windows native + +```powershell +cd C:\Users\Trude\Desktop\vector_explorer +cargo build +``` + +For an optimized build: + +```powershell +cd C:\Users\Trude\Desktop\vector_explorer +cargo build --release +``` + +## How to run + +### WSL + +```bash +cd /mnt/c/Users/Trude/Desktop/vector_explorer +cargo run -- main.sqlite +``` + +To open a different database: + +```bash +cd /mnt/c/Users/Trude/Desktop/vector_explorer +cargo run -- /path/to/other.sqlite +``` + +### Windows native + +```powershell +cd C:\Users\Trude\Desktop\vector_explorer +cargo run -- .\main.sqlite +``` + +## Notes for WSL graphics + +- The app is GTK-based and needs a working GUI/display path in WSL. +- If WSLg or X/Wayland forwarding is not available, the binary can compile successfully but fail to open a display at runtime. +- The app forces `GSK_RENDERER=cairo` by default to avoid flaky EGL/Zink GPU paths commonly seen on WSL. + +## Development workflow + +Useful commands: + +```bash +cargo fmt +cargo check +cargo build +``` + +## Repository contents + +- [src/main.rs](C:/Users/Trude/Desktop/vector_explorer/src/main.rs): GTK application and UI behavior +- [src/store.rs](C:/Users/Trude/Desktop/vector_explorer/src/store.rs): SQLite schema detection and adapter loading +- [src/projection.rs](C:/Users/Trude/Desktop/vector_explorer/src/projection.rs): embedding projection for the semantic map +- [main.sqlite](C:/Users/Trude/Desktop/vector_explorer/main.sqlite): sample OpenClaw-style memory database diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..b61171c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,990 @@ +mod projection; +mod store; + +use std::cell::RefCell; +use std::path::{Path, PathBuf}; +use std::rc::Rc; + +use chrono::{DateTime, Utc}; +use gtk::cairo::Context; +use gtk::gio; +use gtk::prelude::*; +use gtk::{ + Align, Application, ApplicationWindow, Box as GtkBox, Button, DrawingArea, Entry, Frame, + GestureClick, HeaderBar, Label, ListBox, Orientation, Paned, PolicyType, Scale, ScrolledWindow, + SelectionMode, TextView, WrapMode, +}; +use gtk4 as gtk; + +use crate::projection::ProjectedPoint; +use crate::store::{ChunkRecord, FileRecord, LoadedStore, detect_and_load}; + +#[derive(Clone)] +struct AppWidgets { + window: ApplicationWindow, + open_button: Button, + clear_button: Button, + path_label: Label, + adapter_label: Label, + stats_label: Label, + models_label: Label, + backend_label: Label, + notes_label: Label, + filter_entry: Entry, + file_list: ListBox, + yaw_scale: Scale, + pitch_scale: Scale, + plot_area: DrawingArea, + selection_label: Label, + meta_label: Label, + chunk_view: TextView, + tables_label: Label, +} + +struct AppState { + store: Option, + selected_file: Option, + selected_chunk_id: Option, + filter_text: String, + yaw_deg: f64, + pitch_deg: f64, +} + +impl Default for AppState { + fn default() -> Self { + Self { + store: None, + selected_file: None, + selected_chunk_id: None, + filter_text: String::new(), + yaw_deg: 28.0, + pitch_deg: -18.0, + } + } +} + +struct Runtime { + widgets: AppWidgets, + state: Rc>, +} + +thread_local! { + static RUNTIME: RefCell>> = const { RefCell::new(None) }; +} + +fn main() { + if std::env::var_os("GSK_RENDERER").is_none() { + // The viewer draws with Cairo; forcing the Cairo renderer avoids flaky EGL/GPU paths on WSL. + unsafe { + std::env::set_var("GSK_RENDERER", "cairo"); + } + } + + let application = Application::builder() + .application_id("ai.openclaw.vector-explorer") + .flags(gio::ApplicationFlags::HANDLES_OPEN) + .build(); + + application.connect_activate(|application| { + let runtime = ensure_runtime(application); + if runtime.state.borrow().store.is_none() { + if let Some(start_path) = startup_path() { + load_database(&runtime.widgets, &runtime.state, &start_path); + } else { + runtime + .widgets + .path_label + .set_text("Open a SQLite memory file to begin."); + } + } + runtime.widgets.window.present(); + }); + application.connect_open(|application, files, _| { + let runtime = ensure_runtime(application); + if let Some(path) = files.first().and_then(|file| file.path()) { + load_database(&runtime.widgets, &runtime.state, &path); + } + runtime.widgets.window.present(); + }); + application.run(); +} + +fn ensure_runtime(application: &Application) -> Rc { + RUNTIME.with(|runtime| { + if let Some(existing) = runtime.borrow().as_ref() { + return existing.clone(); + } + let widgets = build_widgets(application); + let state = Rc::new(RefCell::new(AppState::default())); + wire_events(&widgets, &state); + let created = Rc::new(Runtime { widgets, state }); + runtime.replace(Some(created.clone())); + created + }) +} + +fn build_widgets(application: &Application) -> AppWidgets { + let window = ApplicationWindow::builder() + .application(application) + .title("Vector Explorer") + .default_width(1440) + .default_height(920) + .build(); + + let open_button = Button::with_label("Open Database"); + let clear_button = Button::with_label("Clear File Filter"); + let filter_entry = Entry::builder() + .placeholder_text("Filter by path, source, model, or chunk text") + .hexpand(true) + .build(); + + let header = HeaderBar::builder().show_title_buttons(true).build(); + header.pack_start(&open_button); + header.pack_start(&clear_button); + header.pack_end(&filter_entry); + window.set_titlebar(Some(&header)); + + let root = GtkBox::new(Orientation::Vertical, 12); + root.set_margin_top(12); + root.set_margin_bottom(12); + root.set_margin_start(12); + root.set_margin_end(12); + + let path_label = Label::builder() + .xalign(0.0) + .wrap(true) + .selectable(true) + .build(); + let adapter_label = Label::builder().xalign(0.0).build(); + let stats_label = Label::builder().xalign(0.0).build(); + let models_label = Label::builder().xalign(0.0).wrap(true).build(); + let backend_label = Label::builder().xalign(0.0).wrap(true).build(); + let notes_label = Label::builder().xalign(0.0).wrap(true).build(); + + let overview_frame = Frame::builder().label("Overview").build(); + let overview_box = GtkBox::new(Orientation::Vertical, 8); + overview_box.append(&path_label); + overview_box.append(&adapter_label); + overview_box.append(&stats_label); + overview_box.append(&models_label); + overview_box.append(&backend_label); + overview_box.append(¬es_label); + overview_frame.set_child(Some(&overview_box)); + root.append(&overview_frame); + + let file_list = ListBox::new(); + file_list.set_selection_mode(SelectionMode::None); + let file_scroller = ScrolledWindow::builder() + .hscrollbar_policy(PolicyType::Never) + .min_content_width(300) + .build(); + file_scroller.set_child(Some(&file_list)); + let files_frame = Frame::builder().label("Files").build(); + files_frame.set_child(Some(&file_scroller)); + + let yaw_scale = Scale::with_range(Orientation::Horizontal, -180.0, 180.0, 1.0); + yaw_scale.set_value(28.0); + yaw_scale.set_hexpand(true); + let pitch_scale = Scale::with_range(Orientation::Horizontal, -75.0, 75.0, 1.0); + pitch_scale.set_value(-18.0); + pitch_scale.set_hexpand(true); + let plot_area = DrawingArea::builder() + .content_width(800) + .content_height(640) + .hexpand(true) + .vexpand(true) + .build(); + let controls_box = GtkBox::new(Orientation::Horizontal, 8); + controls_box.append(&Label::builder().label("Yaw").build()); + controls_box.append(&yaw_scale); + controls_box.append(&Label::builder().label("Pitch").build()); + controls_box.append(&pitch_scale); + let plot_panel = GtkBox::new(Orientation::Vertical, 8); + plot_panel.append(&controls_box); + plot_panel.append(&plot_area); + let plot_frame = Frame::builder().label("Semantic Map").build(); + plot_frame.set_child(Some(&plot_panel)); + + let selection_label = Label::builder() + .xalign(0.0) + .wrap(true) + .selectable(true) + .build(); + let meta_label = Label::builder() + .xalign(0.0) + .wrap(true) + .selectable(true) + .build(); + let chunk_view = TextView::builder() + .editable(false) + .monospace(true) + .wrap_mode(WrapMode::WordChar) + .vexpand(true) + .build(); + let chunk_scroller = ScrolledWindow::builder().min_content_width(360).build(); + chunk_scroller.set_child(Some(&chunk_view)); + let tables_label = Label::builder() + .xalign(0.0) + .wrap(true) + .selectable(true) + .build(); + + let detail_box = GtkBox::new(Orientation::Vertical, 8); + detail_box.append(&selection_label); + detail_box.append(&meta_label); + detail_box.append(&chunk_scroller); + detail_box.append(&tables_label); + let detail_frame = Frame::builder().label("Selection").build(); + detail_frame.set_child(Some(&detail_box)); + + let center_pane = Paned::builder() + .orientation(Orientation::Horizontal) + .start_child(&plot_frame) + .end_child(&detail_frame) + .wide_handle(true) + .shrink_start_child(false) + .build(); + center_pane.set_position(900); + + let main_pane = Paned::builder() + .orientation(Orientation::Horizontal) + .start_child(&files_frame) + .end_child(¢er_pane) + .wide_handle(true) + .build(); + main_pane.set_position(330); + + root.append(&main_pane); + window.set_child(Some(&root)); + + open_button.set_valign(Align::Center); + clear_button.set_valign(Align::Center); + + let widgets = AppWidgets { + window, + open_button, + clear_button, + path_label, + adapter_label, + stats_label, + models_label, + backend_label, + notes_label, + filter_entry, + file_list, + yaw_scale, + pitch_scale, + plot_area, + selection_label, + meta_label, + chunk_view, + tables_label, + }; + widgets +} + +fn wire_events(widgets: &AppWidgets, state: &Rc>) { + { + let widgets = widgets.clone(); + let open_button = widgets.open_button.clone(); + let state = Rc::clone(state); + open_button.connect_clicked(move |_| { + if let Some(path) = rfd::FileDialog::new() + .add_filter("SQLite database", &["sqlite", "db"]) + .set_directory(current_directory()) + .pick_file() + { + load_database(&widgets, &state, &path); + } + }); + } + + { + let widgets = widgets.clone(); + let clear_button = widgets.clear_button.clone(); + let state = Rc::clone(state); + clear_button.connect_clicked(move |_| { + state.borrow_mut().selected_file = None; + let state_ref = state.borrow(); + refresh_files(&widgets, &state_ref, &state); + refresh_selection(&widgets, &state_ref); + widgets.plot_area.queue_draw(); + }); + } + + { + let widgets = widgets.clone(); + let filter_entry = widgets.filter_entry.clone(); + let state = Rc::clone(state); + filter_entry.connect_changed(move |entry| { + state.borrow_mut().filter_text = entry.text().to_string(); + let state_ref = state.borrow(); + refresh_files(&widgets, &state_ref, &state); + refresh_selection(&widgets, &state_ref); + widgets.plot_area.queue_draw(); + }); + } + + { + let widgets = widgets.clone(); + let state = Rc::clone(state); + let yaw_scale = widgets.yaw_scale.clone(); + yaw_scale.connect_value_changed(move |scale| { + state.borrow_mut().yaw_deg = scale.value(); + widgets.plot_area.queue_draw(); + }); + } + + { + let widgets = widgets.clone(); + let state = Rc::clone(state); + let pitch_scale = widgets.pitch_scale.clone(); + pitch_scale.connect_value_changed(move |scale| { + state.borrow_mut().pitch_deg = scale.value(); + widgets.plot_area.queue_draw(); + }); + } + + { + let widgets = widgets.clone(); + let state = Rc::clone(state); + widgets + .plot_area + .set_draw_func(move |_, context, width, height| { + draw_plot(context, width, height, &state.borrow()); + }); + } + + { + let widgets = widgets.clone(); + let state = Rc::clone(state); + let plot_area = widgets.plot_area.clone(); + let click = GestureClick::new(); + click.connect_pressed(move |_, _, x, y| { + let width = widgets.plot_area.width() as f64; + let height = widgets.plot_area.height() as f64; + let chunk_id = { + let state_ref = state.borrow(); + nearest_chunk_at(&state_ref, x, y, width, height, 12.0) + }; + if let Some(chunk_id) = chunk_id { + let mut state_ref = state.borrow_mut(); + state_ref.selected_chunk_id = Some(chunk_id.clone()); + drop(state_ref); + let state_ref = state.borrow(); + refresh_files(&widgets, &state_ref, &state); + refresh_selection(&widgets, &state_ref); + widgets.plot_area.queue_draw(); + } + }); + plot_area.add_controller(click); + } +} + +fn load_database(widgets: &AppWidgets, state: &Rc>, path: &Path) { + match detect_and_load(path) { + Ok(store) => { + widgets.filter_entry.set_text(""); + { + let mut state_ref = state.borrow_mut(); + state_ref.filter_text.clear(); + state_ref.store = Some(store); + state_ref.selected_file = None; + let selected_chunk = state_ref + .store + .as_ref() + .and_then(|store| store.chunks.first().map(|chunk| chunk.id.clone())); + state_ref.selected_chunk_id = selected_chunk; + } + let state_ref = state.borrow(); + refresh_all(widgets, &state_ref, state); + } + Err(error) => { + let mut state_ref = state.borrow_mut(); + state_ref.store = None; + state_ref.selected_file = None; + state_ref.selected_chunk_id = None; + drop(state_ref); + widgets.path_label.set_text(&format!( + "Failed to open {}: {error:#}", + path.to_string_lossy() + )); + widgets.adapter_label.set_text(""); + widgets.stats_label.set_text(""); + widgets.models_label.set_text(""); + widgets.backend_label.set_text(""); + widgets.notes_label.set_text(""); + widgets.selection_label.set_text(""); + widgets.meta_label.set_text(""); + widgets.tables_label.set_text(""); + widgets.chunk_view.buffer().set_text(""); + clear_list_box(&widgets.file_list); + widgets.plot_area.queue_draw(); + } + } +} + +fn refresh_all(widgets: &AppWidgets, state: &AppState, shared_state: &Rc>) { + refresh_overview(widgets, state); + refresh_files(widgets, state, shared_state); + refresh_selection(widgets, state); + widgets.plot_area.queue_draw(); +} + +fn refresh_overview(widgets: &AppWidgets, state: &AppState) { + if let Some(store) = state.store.as_ref() { + widgets + .path_label + .set_text(&format!("Database: {}", store.db_path.display())); + widgets + .adapter_label + .set_text(&format!("Adapter: {}", store.adapter_name)); + widgets.stats_label.set_text(&format!( + "Files: {} Chunks: {} Embeddings: {} Dims: {}", + store.metrics.total_files, + store.metrics.total_chunks, + store.metrics.embedding_rows, + store + .metrics + .embedding_dims + .map(|dims| dims.to_string()) + .unwrap_or_else(|| "unknown".to_string()) + )); + widgets.models_label.set_text(&format!( + "Models: {}", + join_or_unknown(&store.metrics.models) + )); + widgets.backend_label.set_text(&format!( + "Backend: {} Sources: {}", + store + .metrics + .vector_backend + .clone() + .unwrap_or_else(|| "not inferred".to_string()), + join_or_unknown(&store.metrics.sources) + )); + widgets + .notes_label + .set_text(&format!("Notes: {}", store.notes.join(" "))); + } +} + +fn refresh_files(widgets: &AppWidgets, state: &AppState, shared_state: &Rc>) { + clear_list_box(&widgets.file_list); + + let reset_title = Label::builder() + .xalign(0.0) + .wrap(true) + .selectable(false) + .label(if state.selected_file.is_none() { + "Showing all chunks" + } else { + "Show all chunks" + }) + .build(); + let reset_detail = Label::builder() + .xalign(0.0) + .label("Reset file focus and return to the full semantic map") + .build(); + let reset_box = GtkBox::new(Orientation::Vertical, 4); + reset_box.append(&reset_title); + reset_box.append(&reset_detail); + reset_box.set_margin_top(8); + reset_box.set_margin_bottom(8); + reset_box.set_margin_start(8); + reset_box.set_margin_end(8); + let reset_widgets = widgets.clone(); + let reset_state = Rc::clone(shared_state); + let reset_button = Button::new(); + reset_button.set_halign(Align::Fill); + reset_button.set_hexpand(true); + reset_button.add_css_class("flat"); + reset_button.set_child(Some(&reset_box)); + reset_button.connect_clicked(move |_| { + { + let mut state_ref = reset_state.borrow_mut(); + state_ref.selected_file = None; + } + let state_ref = reset_state.borrow(); + refresh_files(&reset_widgets, &state_ref, &reset_state); + refresh_selection(&reset_widgets, &state_ref); + reset_widgets.plot_area.queue_draw(); + }); + widgets.file_list.append(&reset_button); + + for file in filtered_files(state) { + let title_text = if state.selected_file.as_deref() == Some(file.path.as_str()) { + format!("Focused: {}", file.path) + } else { + file.path.clone() + }; + let title = Label::builder() + .xalign(0.0) + .wrap(true) + .selectable(false) + .label(&title_text) + .build(); + let detail = Label::builder() + .xalign(0.0) + .label(&format!( + "{} chunks {} {}", + file.chunk_count, + file.size + .map(|size| format!("{} bytes", size)) + .unwrap_or_else(|| "size unknown".to_string()), + file.mtime_ms + .map(format_timestamp_ms) + .unwrap_or_else(|| "time unknown".to_string()) + )) + .build(); + let path = file.path.clone(); + let widgets_for_click = widgets.clone(); + let state_for_click = Rc::clone(shared_state); + let row_box = GtkBox::new(Orientation::Vertical, 4); + row_box.append(&title); + row_box.append(&detail); + row_box.set_margin_top(8); + row_box.set_margin_bottom(8); + row_box.set_margin_start(8); + row_box.set_margin_end(8); + let row_button = Button::new(); + row_button.set_halign(Align::Fill); + row_button.set_hexpand(true); + row_button.add_css_class("flat"); + row_button.set_child(Some(&row_box)); + row_button.connect_clicked(move |_| { + { + let mut state_ref = state_for_click.borrow_mut(); + state_ref.selected_file = Some(path.clone()); + let selected_chunk = state_ref.store.as_ref().and_then(|store| { + store + .chunks + .iter() + .find(|chunk| chunk.path == path) + .map(|chunk| chunk.id.clone()) + }); + state_ref.selected_chunk_id = selected_chunk; + } + let state_ref = state_for_click.borrow(); + refresh_files(&widgets_for_click, &state_ref, &state_for_click); + refresh_selection(&widgets_for_click, &state_ref); + widgets_for_click.plot_area.queue_draw(); + }); + widgets.file_list.append(&row_button); + } +} + +fn refresh_selection(widgets: &AppWidgets, state: &AppState) { + let Some(store) = state.store.as_ref() else { + widgets.selection_label.set_text(""); + widgets.meta_label.set_text(""); + widgets.tables_label.set_text(""); + widgets.chunk_view.buffer().set_text(""); + return; + }; + + if let Some(chunk) = selected_chunk(state) { + widgets.selection_label.set_text(&format!( + "{}\n{}", + chunk.path, + preview_text(&chunk.text, 180) + )); + widgets.meta_label.set_text(&format!( + "Chunk: {}\nSource: {}\nModel: {}\nLines: {}\nUpdated: {}\nEmbedding dims: {}", + chunk.id, + chunk + .source + .clone() + .unwrap_or_else(|| "unknown".to_string()), + chunk.model.clone().unwrap_or_else(|| "unknown".to_string()), + format_line_range(chunk), + chunk + .updated_at_ms + .map(format_timestamp_ms) + .unwrap_or_else(|| "unknown".to_string()), + chunk + .embedding + .as_ref() + .map(|embedding| embedding.len().to_string()) + .unwrap_or_else(|| "none".to_string()) + )); + widgets.chunk_view.buffer().set_text(&chunk.text); + } else if let Some(file) = selected_file(state) { + widgets.selection_label.set_text(&file.path); + widgets.meta_label.set_text(&format!( + "Source: {}\nChunks: {}\nSize: {}\nUpdated: {}", + file.source.clone().unwrap_or_else(|| "unknown".to_string()), + file.chunk_count, + file.size + .map(|size| format!("{} bytes", size)) + .unwrap_or_else(|| "unknown".to_string()), + file.mtime_ms + .map(format_timestamp_ms) + .unwrap_or_else(|| "unknown".to_string()) + )); + widgets.chunk_view.buffer().set_text( + &store + .chunks + .iter() + .filter(|chunk| chunk.path == file.path) + .map(|chunk| preview_text(&chunk.text, 160)) + .take(3) + .collect::>() + .join("\n\n"), + ); + } else { + widgets.selection_label.set_text("Nothing selected."); + widgets.meta_label.set_text(""); + widgets.chunk_view.buffer().set_text(""); + } + + let table_summary = store + .tables + .iter() + .take(10) + .map(|table| { + format!( + "{} ({}) [{}]", + table.name, + table + .row_count + .map(|count| count.to_string()) + .unwrap_or_else(|| "unreadable".to_string()), + table.columns.join(", ") + ) + }) + .collect::>() + .join("\n"); + widgets + .tables_label + .set_text(&format!("Tables:\n{}", table_summary)); +} + +fn filtered_files(state: &AppState) -> Vec { + let Some(store) = state.store.as_ref() else { + return Vec::new(); + }; + let filter = state.filter_text.to_ascii_lowercase(); + store + .files + .iter() + .filter(|file| { + filter.is_empty() + || file.path.to_ascii_lowercase().contains(&filter) + || file + .source + .as_ref() + .is_some_and(|source| source.to_ascii_lowercase().contains(&filter)) + || store + .chunks + .iter() + .any(|chunk| chunk.path == file.path && chunk_matches_filter(chunk, &filter)) + }) + .cloned() + .collect() +} + +fn visible_points<'a>(state: &'a AppState) -> Vec<(&'a ChunkRecord, &'a ProjectedPoint)> { + let Some(store) = state.store.as_ref() else { + return Vec::new(); + }; + let filter = state.filter_text.to_ascii_lowercase(); + store + .chunks + .iter() + .filter(|chunk| { + state + .selected_file + .as_ref() + .is_none_or(|selected_file| &chunk.path == selected_file) + && (filter.is_empty() || chunk_matches_filter(chunk, &filter)) + }) + .filter_map(|chunk| { + store + .points + .iter() + .find(|point| point.chunk_id == chunk.id) + .map(|point| (chunk, point)) + }) + .collect() +} + +#[derive(Clone, Copy)] +struct RenderedPoint<'a> { + chunk: &'a ChunkRecord, + point: &'a ProjectedPoint, + canvas_x: f64, + canvas_y: f64, + depth: f64, + radius: f64, +} + +fn rendered_points<'a>(state: &'a AppState, width: f64, height: f64) -> Vec> { + let mut rendered = visible_points(state) + .into_iter() + .map(|(chunk, point)| { + let (view_x, view_y, depth, scale) = rotated_view(point, state); + let (canvas_x, canvas_y) = point_to_canvas(view_x, view_y, width, height); + RenderedPoint { + chunk, + point, + canvas_x, + canvas_y, + depth, + radius: 3.5 + (depth + 1.0) * 2.2 * scale, + } + }) + .collect::>(); + rendered.sort_by(|left, right| { + left.depth + .partial_cmp(&right.depth) + .unwrap_or(std::cmp::Ordering::Equal) + }); + rendered +} + +fn draw_plot(context: &Context, width: i32, height: i32, state: &AppState) { + context.set_source_rgb(0.97, 0.97, 0.95); + let _ = context.paint(); + + if state.store.is_none() { + context.set_source_rgb(0.20, 0.20, 0.20); + context.move_to(30.0, 40.0); + let _ = context.show_text("Open a SQLite vector store to draw its chunk map."); + return; + } + + let points = rendered_points(state, width as f64, height as f64); + let margin = 48.0; + let plot_width = (width as f64 - margin * 2.0).max(1.0); + let plot_height = (height as f64 - margin * 2.0).max(1.0); + + context.set_source_rgb(0.88, 0.88, 0.86); + for step in 0..=4 { + let ratio = step as f64 / 4.0; + let x = margin + plot_width * ratio; + let y = margin + plot_height * ratio; + context.move_to(x, margin); + context.line_to(x, margin + plot_height); + context.move_to(margin, y); + context.line_to(margin + plot_width, y); + } + let _ = context.stroke(); + + context.set_source_rgb(0.25, 0.25, 0.25); + draw_axis_guides(context, width as f64, height as f64, state); + + let selected_id = state.selected_chunk_id.as_deref(); + let semantic = points.iter().any(|point| point.point.from_embeddings); + context.move_to(margin, 24.0); + let _ = context.show_text(if semantic { + "3D semantic view from embeddings" + } else { + "3D fallback layout because embeddings were unavailable" + }); + + for (index, rendered) in points.iter().enumerate() { + let radius = if selected_id == Some(rendered.chunk.id.as_str()) { + rendered.radius + 3.0 + } else { + rendered.radius + }; + let color = color_for_index(index, points.len().max(1), rendered.depth); + context.set_source_rgba( + color.0, + color.1, + color.2, + 0.55 + ((rendered.depth + 1.0) / 2.0) * 0.4, + ); + context.arc( + rendered.canvas_x, + rendered.canvas_y, + radius, + 0.0, + std::f64::consts::TAU, + ); + let _ = context.fill(); + } +} + +fn nearest_chunk_at( + state: &AppState, + x: f64, + y: f64, + width: f64, + height: f64, + max_distance: f64, +) -> Option { + rendered_points(state, width, height) + .into_iter() + .map(|rendered| { + let dx = rendered.canvas_x - x; + let dy = rendered.canvas_y - y; + (rendered.chunk.id.clone(), (dx * dx + dy * dy).sqrt()) + }) + .filter(|(_, distance)| *distance <= max_distance) + .min_by(|left, right| { + left.1 + .partial_cmp(&right.1) + .unwrap_or(std::cmp::Ordering::Equal) + }) + .map(|(chunk_id, _)| chunk_id) +} + +fn point_to_canvas(view_x: f64, view_y: f64, width: f64, height: f64) -> (f64, f64) { + let margin = 48.0; + let plot_width = (width - margin * 2.0).max(1.0); + let plot_height = (height - margin * 2.0).max(1.0); + let x = margin + ((view_x + 1.0) / 2.0) * plot_width; + let y = margin + (1.0 - (view_y + 1.0) / 2.0) * plot_height; + (x, y) +} + +fn rotated_view(point: &ProjectedPoint, state: &AppState) -> (f64, f64, f64, f64) { + let yaw = state.yaw_deg.to_radians(); + let pitch = state.pitch_deg.to_radians(); + + let x = point.x as f64; + let y = point.y as f64; + let z = point.z as f64; + + let yaw_x = x * yaw.cos() + z * yaw.sin(); + let yaw_z = -x * yaw.sin() + z * yaw.cos(); + let pitch_y = y * pitch.cos() - yaw_z * pitch.sin(); + let pitch_z = y * pitch.sin() + yaw_z * pitch.cos(); + + let camera_distance = 3.2; + let perspective = camera_distance / (camera_distance - pitch_z * 0.9); + let view_x = (yaw_x * perspective).clamp(-1.2, 1.2); + let view_y = (pitch_y * perspective).clamp(-1.2, 1.2); + (view_x, view_y, pitch_z.clamp(-1.0, 1.0), perspective) +} + +fn draw_axis_guides(context: &Context, width: f64, height: f64, state: &AppState) { + let axes = [ + ("X", 1.0_f32, 0.0_f32, 0.0_f32), + ("Y", 0.0_f32, 1.0_f32, 0.0_f32), + ("Z", 0.0_f32, 0.0_f32, 1.0_f32), + ]; + let origin = ProjectedPoint { + chunk_id: String::new(), + x: 0.0, + y: 0.0, + z: 0.0, + from_embeddings: true, + }; + let (origin_x, origin_y, _, _) = rotated_view(&origin, state); + let (origin_x, origin_y) = point_to_canvas(origin_x, origin_y, width, height); + + context.set_source_rgba(0.20, 0.20, 0.20, 0.7); + for (label, x, y, z) in axes { + let axis_point = ProjectedPoint { + chunk_id: String::new(), + x, + y, + z, + from_embeddings: true, + }; + let (axis_x, axis_y, _, _) = rotated_view(&axis_point, state); + let (axis_x, axis_y) = point_to_canvas(axis_x, axis_y, width, height); + context.move_to(origin_x, origin_y); + context.line_to(axis_x, axis_y); + let _ = context.stroke(); + context.move_to(axis_x + 4.0, axis_y - 4.0); + let _ = context.show_text(label); + } +} + +fn selected_chunk<'a>(state: &'a AppState) -> Option<&'a ChunkRecord> { + let store = state.store.as_ref()?; + let selected_id = state.selected_chunk_id.as_ref()?; + store.chunks.iter().find(|chunk| &chunk.id == selected_id) +} + +fn selected_file<'a>(state: &'a AppState) -> Option<&'a FileRecord> { + let store = state.store.as_ref()?; + let selected_path = state.selected_file.as_ref()?; + store.files.iter().find(|file| &file.path == selected_path) +} + +fn chunk_matches_filter(chunk: &ChunkRecord, filter: &str) -> bool { + chunk.path.to_ascii_lowercase().contains(filter) + || chunk.text.to_ascii_lowercase().contains(filter) + || chunk + .model + .as_ref() + .is_some_and(|model| model.to_ascii_lowercase().contains(filter)) + || chunk + .source + .as_ref() + .is_some_and(|source| source.to_ascii_lowercase().contains(filter)) +} + +fn clear_list_box(list_box: &ListBox) { + while let Some(child) = list_box.first_child() { + list_box.remove(&child); + } +} + +fn preview_text(text: &str, max_chars: usize) -> String { + let compact = text.replace("\r\n", "\n"); + let trimmed = compact.trim(); + if trimmed.chars().count() <= max_chars { + trimmed.to_string() + } else { + format!("{}...", trimmed.chars().take(max_chars).collect::()) + } +} + +fn format_line_range(chunk: &ChunkRecord) -> String { + match (chunk.start_line, chunk.end_line) { + (Some(start), Some(end)) if start == end => start.to_string(), + (Some(start), Some(end)) => format!("{start}-{end}"), + _ => "unknown".to_string(), + } +} + +fn format_timestamp_ms(value: i64) -> String { + DateTime::::from_timestamp_millis(value) + .map(|time| time.format("%Y-%m-%d %H:%M:%S UTC").to_string()) + .unwrap_or_else(|| value.to_string()) +} + +fn join_or_unknown(values: &[String]) -> String { + if values.is_empty() { + "unknown".to_string() + } else { + values.join(", ") + } +} + +fn color_for_index(index: usize, total: usize, depth: f64) -> (f64, f64, f64) { + let hue = (index as f64 / total as f64) * 0.78; + let value = 0.55 + ((depth + 1.0) / 2.0) * 0.30; + hsv_to_rgb(hue, 0.70, value) +} + +fn hsv_to_rgb(hue: f64, saturation: f64, value: f64) -> (f64, f64, f64) { + let section = (hue * 6.0).floor(); + let fraction = hue * 6.0 - section; + let p = value * (1.0 - saturation); + let q = value * (1.0 - fraction * saturation); + let t = value * (1.0 - (1.0 - fraction) * saturation); + match section as i32 % 6 { + 0 => (value, t, p), + 1 => (q, value, p), + 2 => (p, value, t), + 3 => (p, q, value), + 4 => (t, p, value), + _ => (value, p, q), + } +} + +fn startup_path() -> Option { + let default = current_directory().join("main.sqlite"); + default.exists().then_some(default) +} + +fn current_directory() -> PathBuf { + std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")) +} diff --git a/src/projection.rs b/src/projection.rs new file mode 100644 index 0000000..77a57eb --- /dev/null +++ b/src/projection.rs @@ -0,0 +1,223 @@ +use crate::store::ChunkRecord; + +#[derive(Clone, Debug)] +pub struct ProjectedPoint { + pub chunk_id: String, + pub x: f32, + pub y: f32, + pub z: f32, + pub from_embeddings: bool, +} + +pub fn project_chunks(chunks: &[ChunkRecord]) -> Vec { + let dims = dominant_embedding_dimension(chunks); + if let Some(dims) = dims { + let embedding_points = chunks + .iter() + .filter_map(|chunk| { + chunk.embedding.as_ref().and_then(|embedding| { + (embedding.len() == dims).then(|| (chunk.id.clone(), embedding.clone())) + }) + }) + .collect::>(); + + if embedding_points.len() >= 3 { + let embeddings = embedding_points + .iter() + .map(|(_, embedding)| embedding.clone()) + .collect::>(); + let (component_x, component_y, component_z, mean) = principal_components(&embeddings); + let mut scores = embedding_points + .iter() + .map(|(chunk_id, embedding)| { + let centered = embedding + .iter() + .zip(mean.iter()) + .map(|(value, avg)| *value - *avg) + .collect::>(); + let x = dot(¢ered, &component_x); + let y = dot(¢ered, &component_y); + let z = dot(¢ered, &component_z); + (chunk_id.clone(), x, y, z) + }) + .collect::>(); + normalize_scores(&mut scores); + + let mut projected = scores + .into_iter() + .map(|(chunk_id, x, y, z)| ProjectedPoint { + chunk_id, + x, + y, + z, + from_embeddings: true, + }) + .collect::>(); + let existing_ids = projected + .iter() + .map(|point| point.chunk_id.clone()) + .collect::>(); + projected.extend( + chunks + .iter() + .filter(|chunk| !existing_ids.contains(&chunk.id)) + .enumerate() + .map(|(index, chunk)| fallback_point(chunk, index, chunks.len())), + ); + return projected; + } + } + + chunks + .iter() + .enumerate() + .map(|(index, chunk)| fallback_point(chunk, index, chunks.len())) + .collect() +} + +fn fallback_point(chunk: &ChunkRecord, index: usize, total: usize) -> ProjectedPoint { + let columns = (total as f32).sqrt().ceil().max(1.0) as usize; + let row = index / columns; + let column = index % columns; + let width = columns.max(1) as f32; + let height = ((total + columns - 1) / columns).max(1) as f32; + let x = if width <= 1.0 { + 0.0 + } else { + (column as f32 / (width - 1.0)) * 2.0 - 1.0 + }; + let y = if height <= 1.0 { + 0.0 + } else { + (row as f32 / (height - 1.0)) * 2.0 - 1.0 + }; + let z = if total <= 1 { + 0.0 + } else { + (index as f32 / (total as f32 - 1.0)) * 2.0 - 1.0 + }; + ProjectedPoint { + chunk_id: chunk.id.clone(), + x, + y, + z, + from_embeddings: false, + } +} + +fn dominant_embedding_dimension(chunks: &[ChunkRecord]) -> Option { + let mut dims = std::collections::BTreeMap::::new(); + for embedding in chunks.iter().filter_map(|chunk| chunk.embedding.as_ref()) { + *dims.entry(embedding.len()).or_default() += 1; + } + dims.into_iter() + .max_by_key(|(_, count)| *count) + .map(|(dims, _)| dims) +} + +fn principal_components(rows: &[Vec]) -> (Vec, Vec, Vec, Vec) { + let dims = rows.first().map(|row| row.len()).unwrap_or(0); + let mean = mean_vector(rows, dims); + let first = power_iteration(rows, &mean, &[]); + let second = power_iteration(rows, &mean, &[first.as_slice()]); + let third = power_iteration(rows, &mean, &[first.as_slice(), second.as_slice()]); + (first, second, third, mean) +} + +fn mean_vector(rows: &[Vec], dims: usize) -> Vec { + let mut mean = vec![0.0; dims]; + for row in rows { + for (index, value) in row.iter().enumerate() { + mean[index] += *value; + } + } + if !rows.is_empty() { + let scale = 1.0 / rows.len() as f32; + for value in &mut mean { + *value *= scale; + } + } + mean +} + +fn power_iteration(rows: &[Vec], mean: &[f32], orthogonal_to: &[&[f32]]) -> Vec { + let dims = mean.len(); + let mut vector = vec![0.0; dims]; + if dims > 0 { + vector[0] = 1.0; + } + + for _ in 0..24 { + let mut next = covariance_mul(rows, mean, &vector); + for orthogonal in orthogonal_to { + orthogonalize(&mut next, orthogonal); + } + let norm = l2_norm(&next); + if norm <= f32::EPSILON { + break; + } + for value in &mut next { + *value /= norm; + } + vector = next; + } + + if l2_norm(&vector) <= f32::EPSILON && dims > 1 { + vector[1] = 1.0; + } + vector +} + +fn covariance_mul(rows: &[Vec], mean: &[f32], vector: &[f32]) -> Vec { + let mut accum = vec![0.0; mean.len()]; + for row in rows { + let centered = row + .iter() + .zip(mean.iter()) + .map(|(value, avg)| *value - *avg) + .collect::>(); + let score = dot(¢ered, vector); + for (index, value) in centered.iter().enumerate() { + accum[index] += *value * score; + } + } + accum +} + +fn normalize_scores(scores: &mut [(String, f32, f32, f32)]) { + let max_x = scores + .iter() + .map(|(_, x, _, _)| x.abs()) + .fold(0.0_f32, f32::max) + .max(1.0); + let max_y = scores + .iter() + .map(|(_, _, y, _)| y.abs()) + .fold(0.0_f32, f32::max) + .max(1.0); + let max_z = scores + .iter() + .map(|(_, _, _, z)| z.abs()) + .fold(0.0_f32, f32::max) + .max(1.0); + for (_, x, y, z) in scores { + *x /= max_x; + *y /= max_y; + *z /= max_z; + } +} + +fn orthogonalize(vector: &mut [f32], basis: &[f32]) { + let projection = dot(vector, basis); + for (value, basis_value) in vector.iter_mut().zip(basis.iter()) { + *value -= basis_value * projection; + } +} + +fn dot(left: &[f32], right: &[f32]) -> f32 { + left.iter().zip(right.iter()).map(|(a, b)| a * b).sum() +} + +fn l2_norm(values: &[f32]) -> f32 { + dot(values, values).sqrt() +} diff --git a/src/store.rs b/src/store.rs new file mode 100644 index 0000000..856f618 --- /dev/null +++ b/src/store.rs @@ -0,0 +1,764 @@ +#![allow(dead_code)] + +use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::path::{Path, PathBuf}; + +use anyhow::{Context, Result, anyhow}; +use rusqlite::types::ValueRef; +use rusqlite::{Connection, Row}; +use serde::Deserialize; + +use crate::projection::{ProjectedPoint, project_chunks}; + +#[derive(Clone, Debug)] +pub struct LoadedStore { + pub db_path: PathBuf, + pub adapter_name: String, + pub metrics: OverviewMetrics, + pub tables: Vec, + pub files: Vec, + pub chunks: Vec, + pub points: Vec, + pub notes: Vec, +} + +#[derive(Clone, Debug, Default)] +pub struct OverviewMetrics { + pub total_files: usize, + pub total_chunks: usize, + pub embedding_rows: usize, + pub embedding_dims: Option, + pub vector_backend: Option, + pub models: Vec, + pub sources: Vec, +} + +#[derive(Clone, Debug)] +pub struct TableSummary { + pub name: String, + pub columns: Vec, + pub create_sql: Option, + pub row_count: Option, +} + +#[derive(Clone, Debug)] +pub struct FileRecord { + pub path: String, + pub source: Option, + pub size: Option, + pub mtime_ms: Option, + pub chunk_count: usize, +} + +#[derive(Clone, Debug)] +pub struct ChunkRecord { + pub id: String, + pub path: String, + pub source: Option, + pub model: Option, + pub start_line: Option, + pub end_line: Option, + pub updated_at_ms: Option, + pub text: String, + pub embedding: Option>, +} + +pub fn detect_and_load(db_path: &Path) -> Result { + let connection = Connection::open(db_path) + .with_context(|| format!("failed to open SQLite database at {}", db_path.display()))?; + let schema = SchemaSnapshot::read(&connection)?; + let adapters: Vec> = + vec![Box::new(OpenClawAdapter), Box::new(GenericSqliteAdapter)]; + + let adapter = adapters + .into_iter() + .find(|adapter| adapter.detect(&schema)) + .ok_or_else(|| anyhow!("no compatible adapter found"))?; + + adapter.load(&connection, db_path, &schema) +} + +trait VectorStoreAdapter { + fn name(&self) -> &'static str; + fn detect(&self, schema: &SchemaSnapshot) -> bool; + fn load( + &self, + connection: &Connection, + db_path: &Path, + schema: &SchemaSnapshot, + ) -> Result; +} + +struct OpenClawAdapter; + +impl VectorStoreAdapter for OpenClawAdapter { + fn name(&self) -> &'static str { + "OpenClaw Memory" + } + + fn detect(&self, schema: &SchemaSnapshot) -> bool { + schema.has_table_with_columns( + "chunks", + &[ + "id", + "path", + "source", + "start_line", + "end_line", + "model", + "text", + "embedding", + ], + ) && schema.has_table_with_columns("files", &["path", "source", "size", "mtime"]) + } + + fn load( + &self, + connection: &Connection, + db_path: &Path, + schema: &SchemaSnapshot, + ) -> Result { + let meta = load_openclaw_meta(connection)?; + let mut files = load_openclaw_files(connection)?; + let chunks = load_openclaw_chunks(connection)?; + files.sort_by(|left, right| { + right + .chunk_count + .cmp(&left.chunk_count) + .then_with(|| left.path.cmp(&right.path)) + }); + + let metrics = build_metrics( + &files, + &chunks, + meta.as_ref().and_then(|meta| meta.vector_dims), + schema, + ); + let notes = build_openclaw_notes(schema, meta.as_ref(), &metrics); + let tables = schema.to_summaries(connection); + let points = project_chunks(&chunks); + + Ok(LoadedStore { + db_path: db_path.to_path_buf(), + adapter_name: self.name().to_string(), + metrics, + tables, + files, + chunks, + points, + notes, + }) + } +} + +struct GenericSqliteAdapter; + +impl VectorStoreAdapter for GenericSqliteAdapter { + fn name(&self) -> &'static str { + "Generic SQLite Vector Store" + } + + fn detect(&self, schema: &SchemaSnapshot) -> bool { + choose_content_mapping(schema).is_some() + } + + fn load( + &self, + connection: &Connection, + db_path: &Path, + schema: &SchemaSnapshot, + ) -> Result { + let mapping = choose_content_mapping(schema) + .ok_or_else(|| anyhow!("unable to find a chunk/content table"))?; + let chunks = load_generic_chunks(connection, &mapping)?; + let files = load_generic_files(connection, schema, &mapping, &chunks)?; + let metrics = build_metrics(&files, &chunks, None, schema); + let mut notes = vec![format!( + "Detected chunk-like table `{}` using heuristic column matching.", + mapping.table_name + )]; + if let Some(vector_backend) = metrics.vector_backend.as_ref() { + notes.push(format!( + "Vector backend inferred from schema artifacts: {}.", + vector_backend + )); + } + let tables = schema.to_summaries(connection); + let points = project_chunks(&chunks); + + Ok(LoadedStore { + db_path: db_path.to_path_buf(), + adapter_name: self.name().to_string(), + metrics, + tables, + files, + chunks, + points, + notes, + }) + } +} + +#[derive(Clone, Debug)] +struct SchemaSnapshot { + tables: Vec, +} + +#[derive(Clone, Debug)] +struct TableSchema { + name: String, + columns: Vec, + create_sql: Option, +} + +impl SchemaSnapshot { + fn read(connection: &Connection) -> Result { + let mut statement = connection.prepare( + "SELECT name, sql FROM sqlite_master WHERE type IN ('table', 'view') ORDER BY name", + )?; + let raw_tables = statement.query_map([], |row| { + Ok((row.get::<_, String>(0)?, row.get::<_, Option>(1)?)) + })?; + + let mut tables = Vec::new(); + for table in raw_tables { + let (name, create_sql) = table?; + let columns = read_columns(connection, &name).unwrap_or_default(); + tables.push(TableSchema { + name, + columns, + create_sql, + }); + } + Ok(Self { tables }) + } + + fn has_table_with_columns(&self, table_name: &str, required: &[&str]) -> bool { + self.table(table_name).is_some_and(|table| { + required + .iter() + .all(|required_name| table.has_column(required_name)) + }) + } + + fn table(&self, table_name: &str) -> Option<&TableSchema> { + self.tables.iter().find(|table| table.name == table_name) + } + + fn to_summaries(&self, connection: &Connection) -> Vec { + self.tables + .iter() + .map(|table| TableSummary { + name: table.name.clone(), + columns: table.columns.clone(), + create_sql: table.create_sql.clone(), + row_count: count_rows(connection, &table.name).ok(), + }) + .collect() + } +} + +impl TableSchema { + fn has_column(&self, column_name: &str) -> bool { + self.columns.iter().any(|column| column == column_name) + } +} + +#[derive(Debug, Deserialize)] +struct OpenClawMeta { + #[serde(default)] + model: Option, + #[serde(default)] + provider: Option, + #[serde(rename = "vectorDims", default)] + vector_dims: Option, +} + +fn load_openclaw_meta(connection: &Connection) -> Result> { + let raw_value = connection + .query_row( + "SELECT value FROM meta WHERE key = 'memory_index_meta_v1'", + [], + |row| row.get::<_, String>(0), + ) + .ok(); + + raw_value + .map(|value| serde_json::from_str::(&value).context("failed to parse meta")) + .transpose() +} + +fn load_openclaw_files(connection: &Connection) -> Result> { + let mut statement = connection.prepare( + "SELECT + f.path, + f.source, + f.size, + CAST(f.mtime AS INTEGER) AS mtime_ms, + COUNT(c.id) AS chunk_count + FROM files f + LEFT JOIN chunks c ON c.path = f.path + GROUP BY f.path, f.source, f.size, f.mtime + ORDER BY f.path", + )?; + let rows = statement.query_map([], |row| { + Ok(FileRecord { + path: row.get(0)?, + source: row.get(1)?, + size: row.get(2)?, + mtime_ms: row.get(3)?, + chunk_count: row.get::<_, i64>(4).unwrap_or_default().max(0) as usize, + }) + })?; + rows.collect::>>() + .map_err(Into::into) +} + +fn load_openclaw_chunks(connection: &Connection) -> Result> { + let mut statement = connection.prepare( + "SELECT + id, + path, + source, + model, + start_line, + end_line, + updated_at, + text, + embedding + FROM chunks + ORDER BY path, start_line, end_line, id", + )?; + let rows = statement.query_map([], decode_chunk_row)?; + rows.collect::>>() + .map_err(Into::into) +} + +fn read_columns(connection: &Connection, table_name: &str) -> Result> { + let pragma = format!("PRAGMA table_info({})", quote_ident(table_name)); + let mut statement = connection.prepare(&pragma)?; + let rows = statement.query_map([], |row| row.get::<_, String>(1))?; + rows.collect::>>() + .map_err(Into::into) +} + +fn count_rows(connection: &Connection, table_name: &str) -> Result { + let sql = format!("SELECT COUNT(*) FROM {}", quote_ident(table_name)); + connection + .query_row(&sql, [], |row| row.get::<_, i64>(0)) + .map_err(Into::into) +} + +fn build_openclaw_notes( + schema: &SchemaSnapshot, + meta: Option<&OpenClawMeta>, + metrics: &OverviewMetrics, +) -> Vec { + let mut notes = Vec::new(); + if let Some(meta) = meta { + if let (Some(provider), Some(model)) = (&meta.provider, &meta.model) { + notes.push(format!( + "Memory index metadata reports provider `{}` with embedding model `{}`.", + provider, model + )); + } + } + if schema.table("chunks_fts").is_some() { + notes.push( + "FTS5 is present, so the store can support lexical search alongside vectors." + .to_string(), + ); + } + if schema.table("chunks_vec").is_some() { + notes.push("`sqlite-vec` artifacts are present, which aligns with OpenClaw's hybrid search layout.".to_string()); + } + notes.push(format!( + "Loaded {} files and {} chunks from the memory index.", + metrics.total_files, metrics.total_chunks + )); + notes +} + +fn build_metrics( + files: &[FileRecord], + chunks: &[ChunkRecord], + meta_dims: Option, + schema: &SchemaSnapshot, +) -> OverviewMetrics { + let mut models = BTreeSet::new(); + let mut sources = BTreeSet::new(); + let mut dims = BTreeMap::::new(); + + for chunk in chunks { + if let Some(model) = chunk.model.as_ref() { + models.insert(model.clone()); + } + if let Some(source) = chunk.source.as_ref() { + sources.insert(source.clone()); + } + if let Some(embedding) = chunk.embedding.as_ref() { + *dims.entry(embedding.len()).or_default() += 1; + } + } + for file in files { + if let Some(source) = file.source.as_ref() { + sources.insert(source.clone()); + } + } + + let vector_backend = detect_vector_backend(schema); + let embedding_dims = meta_dims.or_else(|| { + dims.into_iter() + .max_by_key(|(_, count)| *count) + .map(|(dims, _)| dims) + }); + + OverviewMetrics { + total_files: files.len(), + total_chunks: chunks.len(), + embedding_rows: chunks + .iter() + .filter(|chunk| chunk.embedding.is_some()) + .count(), + embedding_dims, + vector_backend, + models: models.into_iter().collect(), + sources: sources.into_iter().collect(), + } +} + +fn detect_vector_backend(schema: &SchemaSnapshot) -> Option { + let mut backends = Vec::new(); + for table in &schema.tables { + let sql = table + .create_sql + .as_deref() + .unwrap_or_default() + .to_ascii_lowercase(); + if sql.contains("using vec0") { + backends.push("sqlite-vec".to_string()); + } else if sql.contains("using vss0") { + backends.push("sqlite-vss".to_string()); + } else if table.name.contains("fts") { + backends.push("fts5".to_string()); + } + } + backends.sort(); + backends.dedup(); + (!backends.is_empty()).then(|| backends.join(" + ")) +} + +#[derive(Clone, Debug)] +struct GenericContentMapping { + table_name: String, + id_column: Option, + path_column: Option, + text_column: String, + source_column: Option, + model_column: Option, + start_line_column: Option, + end_line_column: Option, + updated_at_column: Option, + embedding_column: Option, +} + +fn choose_content_mapping(schema: &SchemaSnapshot) -> Option { + let mut best_mapping = None; + let mut best_score = i32::MIN; + + for table in &schema.tables { + let lower = table + .columns + .iter() + .map(|column| column.to_ascii_lowercase()) + .collect::>(); + let text_column = find_column( + &lower, + &[ + "text", + "content", + "chunk_text", + "body", + "document", + "payload", + ], + ); + let embedding_column = find_column( + &lower, + &["embedding", "vector", "embedding_json", "embedding_blob"], + ); + let id_column = find_column(&lower, &["id", "chunk_id", "uuid"]); + let path_column = find_column( + &lower, + &[ + "path", + "file_path", + "document_path", + "source_path", + "uri", + "doc_id", + "document_id", + ], + ); + + if let Some(text_index) = text_column { + let mut score = 15; + if embedding_column.is_some() { + score += 20; + } + if id_column.is_some() { + score += 4; + } + if path_column.is_some() { + score += 4; + } + if table.name.contains("chunk") || table.name.contains("embedding") { + score += 3; + } + + if score > best_score { + best_score = score; + best_mapping = Some(GenericContentMapping { + table_name: table.name.clone(), + id_column: id_column.map(|index| table.columns[index].clone()), + path_column: path_column.map(|index| table.columns[index].clone()), + text_column: table.columns[text_index].clone(), + source_column: find_column(&lower, &["source", "provider", "namespace"]) + .map(|index| table.columns[index].clone()), + model_column: find_column(&lower, &["model", "embedding_model"]) + .map(|index| table.columns[index].clone()), + start_line_column: find_column(&lower, &["start_line", "line_start"]) + .map(|index| table.columns[index].clone()), + end_line_column: find_column(&lower, &["end_line", "line_end"]) + .map(|index| table.columns[index].clone()), + updated_at_column: find_column(&lower, &["updated_at", "mtime", "created_at"]) + .map(|index| table.columns[index].clone()), + embedding_column: embedding_column.map(|index| table.columns[index].clone()), + }); + } + } + } + + best_mapping +} + +fn find_column(columns: &[String], candidates: &[&str]) -> Option { + candidates + .iter() + .find_map(|candidate| columns.iter().position(|column| column == candidate)) +} + +fn load_generic_chunks( + connection: &Connection, + mapping: &GenericContentMapping, +) -> Result> { + let sql = format!( + "SELECT + {id_expr} AS item_id, + {path_expr} AS item_path, + {source_expr} AS item_source, + {model_expr} AS item_model, + {start_expr} AS item_start_line, + {end_expr} AS item_end_line, + {updated_expr} AS item_updated_at, + {text_expr} AS item_text, + {embedding_expr} AS item_embedding + FROM {table_name}", + id_expr = mapping + .id_column + .as_ref() + .map(|column| quote_ident(column)) + .unwrap_or_else(|| "CAST(rowid AS TEXT)".to_string()), + path_expr = nullable_ident(mapping.path_column.as_ref()), + source_expr = nullable_ident(mapping.source_column.as_ref()), + model_expr = nullable_ident(mapping.model_column.as_ref()), + start_expr = nullable_ident(mapping.start_line_column.as_ref()), + end_expr = nullable_ident(mapping.end_line_column.as_ref()), + updated_expr = nullable_ident(mapping.updated_at_column.as_ref()), + text_expr = quote_ident(&mapping.text_column), + embedding_expr = nullable_ident(mapping.embedding_column.as_ref()), + table_name = quote_ident(&mapping.table_name), + ); + + let mut statement = connection.prepare(&sql)?; + let rows = statement.query_map([], |row| { + let id = row.get::<_, String>(0)?; + let path = row + .get::<_, Option>(1)? + .unwrap_or_else(|| format!("{}#{}", mapping.table_name, id)); + let source = row.get(2)?; + let model = row.get(3)?; + let start_line = row.get(4)?; + let end_line = row.get(5)?; + let updated_at_ms = row.get(6)?; + let text = row.get::<_, String>(7)?; + let embedding = decode_embedding_value(row, 8)?; + Ok(ChunkRecord { + id, + path, + source, + model, + start_line, + end_line, + updated_at_ms, + text, + embedding, + }) + })?; + rows.collect::>>() + .map_err(Into::into) +} + +fn load_generic_files( + connection: &Connection, + schema: &SchemaSnapshot, + _mapping: &GenericContentMapping, + chunks: &[ChunkRecord], +) -> Result> { + if let Some(file_table) = choose_file_table(schema) { + let sql = format!( + "SELECT + {path_expr} AS file_path, + {source_expr} AS file_source, + {size_expr} AS file_size, + {mtime_expr} AS file_mtime + FROM {table_name}", + path_expr = quote_ident(&file_table.path_column), + source_expr = nullable_ident(file_table.source_column.as_ref()), + size_expr = nullable_ident(file_table.size_column.as_ref()), + mtime_expr = nullable_ident(file_table.mtime_column.as_ref()), + table_name = quote_ident(&file_table.table_name), + ); + let chunk_counts = group_chunk_counts(chunks); + let mut statement = connection.prepare(&sql)?; + let rows = statement.query_map([], |row| { + let path = row.get::<_, String>(0)?; + Ok(FileRecord { + chunk_count: *chunk_counts.get(&path).unwrap_or(&0), + path, + source: row.get(1)?, + size: row.get(2)?, + mtime_ms: row.get(3)?, + }) + })?; + let mut files = rows.collect::>>()?; + files.sort_by(|left, right| left.path.cmp(&right.path)); + return Ok(files); + } + + let chunk_counts = group_chunk_counts(chunks); + let mut files = chunk_counts + .into_iter() + .map(|(path, chunk_count)| FileRecord { + path, + source: None, + size: None, + mtime_ms: None, + chunk_count, + }) + .collect::>(); + files.sort_by(|left, right| left.path.cmp(&right.path)); + Ok(files) +} + +#[derive(Clone, Debug)] +struct FileTableMapping { + table_name: String, + path_column: String, + source_column: Option, + size_column: Option, + mtime_column: Option, +} + +fn choose_file_table(schema: &SchemaSnapshot) -> Option { + schema.tables.iter().find_map(|table| { + let lower = table + .columns + .iter() + .map(|column| column.to_ascii_lowercase()) + .collect::>(); + let path_column = find_column( + &lower, + &["path", "file_path", "document_path", "source_path", "uri"], + )?; + let has_size = find_column(&lower, &["size", "byte_size"]); + let has_mtime = find_column(&lower, &["mtime", "updated_at", "modified_at"]); + if table.name.contains("file") || has_size.is_some() || has_mtime.is_some() { + Some(FileTableMapping { + table_name: table.name.clone(), + path_column: table.columns[path_column].clone(), + source_column: find_column(&lower, &["source", "provider", "namespace"]) + .map(|index| table.columns[index].clone()), + size_column: has_size.map(|index| table.columns[index].clone()), + mtime_column: has_mtime.map(|index| table.columns[index].clone()), + }) + } else { + None + } + }) +} + +fn group_chunk_counts(chunks: &[ChunkRecord]) -> HashMap { + let mut counts = HashMap::new(); + for chunk in chunks { + *counts.entry(chunk.path.clone()).or_default() += 1; + } + counts +} + +fn decode_chunk_row(row: &Row<'_>) -> rusqlite::Result { + Ok(ChunkRecord { + id: row.get(0)?, + path: row.get(1)?, + source: row.get(2)?, + model: row.get(3)?, + start_line: row.get(4)?, + end_line: row.get(5)?, + updated_at_ms: row.get(6)?, + text: row.get(7)?, + embedding: decode_embedding_value(row, 8)?, + }) +} + +fn decode_embedding_value( + row: &Row<'_>, + column_index: usize, +) -> rusqlite::Result>> { + match row.get_ref(column_index)? { + ValueRef::Null => Ok(None), + ValueRef::Text(bytes) => Ok(parse_text_embedding(bytes)), + ValueRef::Blob(bytes) => Ok(parse_blob_embedding(bytes)), + ValueRef::Integer(_) | ValueRef::Real(_) => Ok(None), + } +} + +fn parse_text_embedding(bytes: &[u8]) -> Option> { + let text = std::str::from_utf8(bytes).ok()?; + if text.trim().is_empty() { + return None; + } + serde_json::from_str::>(text) + .ok() + .filter(|values| !values.is_empty()) +} + +fn parse_blob_embedding(bytes: &[u8]) -> Option> { + if bytes.len() % 4 != 0 { + return None; + } + let mut values = Vec::with_capacity(bytes.len() / 4); + for chunk in bytes.chunks_exact(4) { + values.push(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])); + } + (!values.is_empty()).then_some(values) +} + +fn quote_ident(identifier: &str) -> String { + format!("\"{}\"", identifier.replace('"', "\"\"")) +} + +fn nullable_ident(identifier: Option<&String>) -> String { + identifier + .map(|identifier| quote_ident(identifier)) + .unwrap_or_else(|| "NULL".to_string()) +}