commit 23ce1b7ee2b6baec9ac7a47697e84a2ef2f6e4f6 Author: Andreas Tsouchlos Date: Wed Mar 25 09:32:02 2026 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5090ed9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/target +/dist +/src/dist +/src/target +/src-tauri/target +/node_modules +/src-tauri/pdfjs/node_modules +/src-tauri/pdfjs/package-lock.json diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a47588e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/pdfjs-dist"] + path = vendor/pdfjs-dist + url = https://github.com/mozilla/pdfjs-dist diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f2e6ea8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,84 @@ +# Brittle — Literature Management System + +A desktop application for managing academic references, PDFs, and annotations. Written in Rust. + +## Project Vision + +Brittle is a personal literature management tool in the spirit of Zotero. Core capabilities: + +- Organize references in nested libraries (libraries contain sub-libraries) +- Display and annotate PDFs (create, edit, view annotations) +- Export references as BibTeX + +This is a hobby project built primarily through AI-assisted development. The human operator makes architectural decisions; the agent writes the code. + +## Architecture + +### Mandatory: Library-First Design + +The backend is a **Rust crate** (`brittle-core`) that exposes a public API. This is the primary interface. A frontend like [iced](https://github.com/iced-rs/iced) links directly against this crate. + +The API must be designed so that wrapping it in REST, gRPC, IPC, or any other transport layer requires **zero changes to `brittle-core`**. This means: + +- No UI concepts leak into the core (no widget types, no rendering logic, no event loops) +- Return types must be transport-agnostic: no framework-specific channels, callbacks, or handle types. Plain data that any layer can serialize or forward +- Errors are structured and meaningful, not stringly-typed +- The public API surface is the **single source of truth** for what Brittle can do + +If a frontend needs something that isn't in the core API, the correct response is to extend the core API — not to hack around it. + +### Dependency Philosophy + +Use external crates where they provide real value (PDF rendering, BibTeX parsing, SQLite bindings, etc.). Do not pull in a dependency for something the standard library handles or for trivial utility code. When choosing between crates, prefer those that are well-maintained, have minimal transitive dependencies, and are widely used in the Rust ecosystem. + +### Database Versioning + +The database must support some form of version control or change tracking. The specific mechanism (event sourcing, snapshot-based, migration-based, audit log, etc.) is an **open design question** to be resolved during planning before implementation begins. + +## Development Workflow + +Every non-trivial feature follows this process end-to-end. Do not skip steps. + +### 1. Requirements (what, not how) + +Start from user-facing behavior. What does this feature need to do? Define concrete acceptance criteria. Do not think about implementation yet. + +### 2. Architectural Fit + +Before designing anything, assess how this feature relates to what already exists: + +- **Fit**: Does it integrate cleanly with the current structure? +- **Risk**: If restructuring is needed, what existing functionality could break? +- **Benefit**: What do we gain from restructuring vs. working within the current design? +- **Verdict**: Restructure, adapt, or flag as tech debt? + +Write this assessment as part of the plan. Do not silently restructure. + +### 3. Design (top-down) + +Now decide *how*: types, traits, modules, public API surface. Work downward from the API the feature needs, not upward from utility code you think might be useful. + +### 4. Tests + +Write failing tests that encode the acceptance criteria from step 1. These tests define "done." + +### 5. Implementation + +Make the tests pass. Then refactor with confidence. + +Use **Plan mode** at the start of any multi-file task. + +## Agent Behavior + +Do not blindly implement what is asked. Consider whether the request is actually what the user needs, whether there's a better approach, and whether there are edge cases or implications the user may not have considered. + +If you see a better path, **argue for it**. Explain your reasoning clearly. But if the user disagrees after hearing your case, accept their decision and implement it well. + +## Code Style + +- Prioritize readability and testability over cleverness +- Use meaningful names; avoid abbreviations unless they're domain-standard (e.g., `bib`, `doi`, `pdf`) +- Keep functions focused — if a function needs a paragraph-long comment to explain, it should probably be split +- Use Rust idioms: `Result` for fallible operations, strong types over stringly-typed data, `enum` for state machines +- Run `cargo clippy` and `cargo fmt` before considering any task complete +- Document public API items with doc comments diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..871d029 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,5224 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[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 = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[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 = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brittle-app" +version = "0.1.0" +dependencies = [ + "brittle-core", + "dirs", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tempfile", + "thiserror 2.0.18", + "tokio", + "toml 0.8.2", + "url", + "urlencoding", + "uuid", +] + +[[package]] +name = "brittle-core" +version = "0.1.0" +dependencies = [ + "chrono", + "git2", + "serde", + "serde_json", + "sha2", + "tempfile", + "thiserror 2.0.18", + "toml 0.8.2", + "ureq", + "uuid", +] + +[[package]] +name = "brittle-keymap" +version = "0.1.0" + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.11.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.11.0", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.11.0", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.29.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "matches", + "phf 0.10.1", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.0", + "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 2.0.117", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser 0.36.0", + "foldhash 0.2.0", + "html5ever 0.38.0", + "precomputed-hash", + "selectors 0.36.1", + "tendril 0.5.0", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47ec73ddcf6b7f23173d5c3c5a32b5507dc0a734de7730aa14abc5d5e296bb5f" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.9.12+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[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 = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[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 = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[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-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[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.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "git2" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" +dependencies = [ + "bitflags 2.11.0", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.11.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever 0.14.1", + "match_token", +] + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever 0.38.0", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[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 0.62.2", +] + +[[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 = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png", +] + +[[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 = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[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 = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[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 = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[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 = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.11.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kuchikiki" +version = "0.8.8-speedreader" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" +dependencies = [ + "cssparser 0.29.6", + "html5ever 0.29.1", + "indexmap 2.13.0", + "selectors 0.24.0", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libgit2-sys" +version = "0.18.3+1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "libc", +] + +[[package]] +name = "libssh2-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1" +dependencies = [ + "cc", + "libc", + "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 = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache 0.8.9", + "string_cache_codegen 0.5.4", + "tendril 0.4.3", +] + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril 0.5.0", + "web_atoms", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[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 = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.0", + "block2", + "objc2", + "objc2-core-foundation", + "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 2.11.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.0", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.11.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared 0.8.0", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.2", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher 1.0.2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64 0.22.1", + "indexmap 2.13.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.5+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[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.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +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.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[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 2.11.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" +dependencies = [ + "bitflags 1.3.2", + "cssparser 0.29.6", + "derive_more 0.99.20", + "fxhash", + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc 0.2.0", + "smallvec", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.11.0", + "cssparser 0.36.0", + "derive_more 2.1.1", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen 0.13.1", + "precomputed-hash", + "rustc-hash", + "servo_arc 0.4.3", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[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-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[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 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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 2.0.117", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[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 = "serde_with" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "servo_arc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[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 = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.13.1", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[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 = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.34.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9103edf55f2da3c82aea4c7fab7c4241032bfeea0e71fa557d98e00e7ce7cc20" +dependencies = [ + "bitflags 2.11.0", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "parking_lot", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da77cc00fb9028caf5b5d4650f75e31f1ef3693459dfca7f7e506d1ecef0ba2d" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "toml 0.9.12+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.117", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-runtime" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2826d79a3297ed08cd6ea7f412644ef58e32969504bc4fbd8d7dbeabc4445ea2" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dunce", + "glob", + "html5ever 0.29.1", + "http", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.3", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" +dependencies = [ + "dunce", + "embed-resource", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[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 2.13.0", + "serde_core", + "serde_spanned 1.0.4", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[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.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.13.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 1.0.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.0", +] + +[[package]] +name = "toml_parser" +version = "1.0.10+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" +dependencies = [ + "winnow 1.0.0", +] + +[[package]] +name = "toml_writer" +version = "1.0.7+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[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 = "tray-icon" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64 0.22.1", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "url", + "webpki-roots 0.26.11", +] + +[[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 = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "getrandom 0.4.2", + "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 = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.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 2.0.117", + "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 2.13.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + +[[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 = "web_atoms" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" +dependencies = [ + "phf 0.13.1", + "phf_codegen 0.13.1", + "string_cache 0.9.0", + "string_cache_codegen 0.6.1", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.6", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[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 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[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 2.0.117", +] + +[[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 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[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 0.5.0", + "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 0.5.0", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.117", + "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 2.0.117", + "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 2.11.0", + "indexmap 2.13.0", + "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 2.13.0", + "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 = "wry" +version = "0.54.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a8135d8676225e5744de000d4dff5a082501bf7db6a1c1495034f8c314edbc" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[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 2.0.117", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..cd45bd8 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["brittle-core", "brittle-keymap", "src-tauri"] +resolver = "2" diff --git a/Trunk.toml b/Trunk.toml new file mode 100644 index 0000000..97cd938 --- /dev/null +++ b/Trunk.toml @@ -0,0 +1,7 @@ +[build] +target = "src/index.html" +dist = "dist" + +[serve] +port = 1420 +open = false diff --git a/brittle-core/Cargo.toml b/brittle-core/Cargo.toml new file mode 100644 index 0000000..c773355 --- /dev/null +++ b/brittle-core/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "brittle-core" +version = "0.1.0" +edition = "2024" + +[dependencies] +chrono = { version = "0.4", features = ["serde"] } +git2 = { version = "0.20", features = ["vendored-libgit2"] } +serde = { version = "1", features = ["derive"] } +sha2 = "0.10" +thiserror = "2" +toml = "0.8" +ureq = "2" +uuid = { version = "1", features = ["v7", "serde"] } + +[[bin]] +name = "brittle-seed" +path = "src/bin/seed.rs" + +[dev-dependencies] +serde_json = "1" +tempfile = "3" diff --git a/brittle-core/src/bibtex/export.rs b/brittle-core/src/bibtex/export.rs new file mode 100644 index 0000000..53a0d95 --- /dev/null +++ b/brittle-core/src/bibtex/export.rs @@ -0,0 +1,179 @@ +use crate::bibtex::validation::validate_for_export; +use crate::error::BibtexError; +use crate::model::{Person, Reference}; + +/// Escape a BibTeX field value by wrapping special characters in braces. +/// Handles `{`, `}`, `\`, and preserves existing braced groups. +fn escape_field(value: &str) -> String { + // Wrap the whole value in braces — simple and safe for most content. + // This prevents BibTeX from case-folding titles and handles special chars. + format!("{{{value}}}") +} + +/// Format a list of persons as a BibTeX "and"-separated author string. +fn format_persons(persons: &[Person]) -> String { + persons + .iter() + .map(|p| p.to_bibtex()) + .collect::>() + .join(" and ") +} + +/// Export a single reference as a BibTeX entry string. +/// +/// Returns an error if required fields are missing. +pub fn export_reference(reference: &Reference) -> Result { + validate_for_export(reference)?; + + let mut out = String::new(); + + out.push('@'); + out.push_str(reference.entry_type.bibtex_name()); + out.push('{'); + out.push_str(&reference.cite_key); + out.push_str(",\n"); + + // Authors and editors come first for readability. + if !reference.authors.is_empty() { + let authors = format_persons(&reference.authors); + out.push_str(&format!(" author = {},\n", escape_field(&authors))); + } + if !reference.editors.is_empty() { + let editors = format_persons(&reference.editors); + out.push_str(&format!(" editor = {},\n", escape_field(&editors))); + } + + // All other fields in sorted order (BTreeMap guarantees this). + for (key, value) in &reference.fields { + out.push_str(&format!(" {key} = {},\n", escape_field(value))); + } + + out.push('}'); + + Ok(out) +} + +/// Export multiple references as a `.bib` file string. +/// +/// Skips references with missing required fields and collects all errors. +/// Returns the BibTeX string and a list of any export errors. +pub fn export_references(references: &[Reference]) -> (String, Vec) { + let mut out = String::new(); + let mut errors = Vec::new(); + + for reference in references { + match export_reference(reference) { + Ok(entry) => { + if !out.is_empty() { + out.push('\n'); + } + out.push_str(&entry); + out.push('\n'); + } + Err(e) => errors.push(e), + } + } + + (out, errors) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::model::{EntryType, Person, Reference}; + + fn make_article() -> Reference { + let mut r = Reference::new("turing1950", EntryType::Article); + r.authors.push(Person { + family: "Turing".into(), + given: Some("Alan M.".into()), + prefix: None, + suffix: None, + }); + r.fields.insert( + "title".into(), + "Computing Machinery and Intelligence".into(), + ); + r.fields.insert("journal".into(), "Mind".into()); + r.fields.insert("year".into(), "1950".into()); + r.fields.insert("volume".into(), "59".into()); + r + } + + #[test] + fn article_export() { + let r = make_article(); + let bibtex = export_reference(&r).unwrap(); + assert!(bibtex.starts_with("@article{turing1950,")); + assert!(bibtex.contains("author = {Turing, Alan M.}")); + assert!(bibtex.contains("title = {Computing Machinery and Intelligence}")); + assert!(bibtex.contains("journal = {Mind}")); + assert!(bibtex.contains("year = {1950}")); + } + + #[test] + fn multi_author_formatting() { + let mut r = Reference::new("ab2024", EntryType::Article); + r.authors.push(Person { + family: "Doe".into(), + given: Some("Jane".into()), + prefix: None, + suffix: None, + }); + r.authors.push(Person { + family: "Smith".into(), + given: Some("John".into()), + prefix: None, + suffix: None, + }); + r.fields.insert("title".into(), "A Paper".into()); + r.fields.insert("journal".into(), "Nature".into()); + r.fields.insert("year".into(), "2024".into()); + + let bibtex = export_reference(&r).unwrap(); + assert!(bibtex.contains("Doe, Jane and Smith, John")); + } + + #[test] + fn missing_required_field_returns_error() { + let mut r = make_article(); + r.fields.remove("journal"); + assert!(export_reference(&r).is_err()); + } + + #[test] + fn book_with_editor() { + let mut r = Reference::new("knuth1986", EntryType::Book); + r.editors.push(Person::new("Knuth")); + r.fields.insert("title".into(), "The TeXbook".into()); + r.fields.insert("publisher".into(), "Addison-Wesley".into()); + r.fields.insert("year".into(), "1986".into()); + + let bibtex = export_reference(&r).unwrap(); + assert!(bibtex.starts_with("@book{")); + assert!(bibtex.contains("editor = {Knuth}")); + } + + #[test] + fn fields_appear_in_sorted_order() { + let r = make_article(); + let bibtex = export_reference(&r).unwrap(); + let journal_pos = bibtex.find("journal").unwrap(); + let title_pos = bibtex.find("title").unwrap(); + let year_pos = bibtex.find("year").unwrap(); + // BTreeMap order: journal < title < volume < year (alphabetical) + assert!(journal_pos < title_pos); + assert!(title_pos < year_pos); + } + + #[test] + fn export_references_collects_errors() { + let good = make_article(); + let bad = Reference::new("incomplete", EntryType::Article); + // Missing author, title, journal, year + + let (bibtex, errors) = export_references(&[good, bad]); + assert_eq!(errors.len(), 1); + assert!(bibtex.contains("@article{turing1950,")); + } +} diff --git a/brittle-core/src/bibtex/mod.rs b/brittle-core/src/bibtex/mod.rs new file mode 100644 index 0000000..18e637e --- /dev/null +++ b/brittle-core/src/bibtex/mod.rs @@ -0,0 +1,5 @@ +pub mod export; +pub mod validation; + +pub use export::{export_reference, export_references}; +pub use validation::validate_for_export; diff --git a/brittle-core/src/bibtex/validation.rs b/brittle-core/src/bibtex/validation.rs new file mode 100644 index 0000000..181bffc --- /dev/null +++ b/brittle-core/src/bibtex/validation.rs @@ -0,0 +1,104 @@ +use crate::error::BibtexError; +use crate::model::{EntryType, Reference}; + +/// Returns the required fields for a given BibTeX entry type. +fn required_fields(entry_type: &EntryType) -> &'static [&'static str] { + match entry_type { + EntryType::Article => &["author", "title", "journal", "year"], + EntryType::Book => &["title", "publisher", "year"], + EntryType::Booklet => &["title"], + EntryType::InBook => &["title", "publisher", "year", "chapter"], + EntryType::InCollection => &["author", "title", "booktitle", "publisher", "year"], + EntryType::InProceedings => &["author", "title", "booktitle", "year"], + EntryType::Manual => &["title"], + EntryType::MastersThesis => &["author", "title", "school", "year"], + EntryType::Misc => &[], + EntryType::PhdThesis => &["author", "title", "school", "year"], + EntryType::Proceedings => &["title", "year"], + EntryType::TechReport => &["author", "title", "institution", "year"], + EntryType::Unpublished => &["author", "title", "note"], + EntryType::Online => &["title", "url"], + } +} + +/// Validate that a reference has all required fields for BibTeX export. +/// Returns an error describing the first missing required field found. +pub fn validate_for_export(reference: &Reference) -> Result<(), BibtexError> { + let required = required_fields(&reference.entry_type); + + for &field in required { + let present = match field { + "author" => !reference.authors.is_empty(), + "editor" => !reference.editors.is_empty(), + _ => reference.fields.contains_key(field), + }; + if !present { + return Err(BibtexError::MissingRequiredField { + cite_key: reference.cite_key.clone(), + entry_type: reference.entry_type.bibtex_name().to_owned(), + field: field.to_owned(), + }); + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::model::{EntryType, Person, Reference}; + + fn make_article() -> Reference { + let mut r = Reference::new("doe2024", EntryType::Article); + r.authors.push(Person::new("Doe")); + r.fields.insert("title".into(), "A Paper".into()); + r.fields.insert("journal".into(), "Nature".into()); + r.fields.insert("year".into(), "2024".into()); + r + } + + #[test] + fn valid_article_passes() { + let r = make_article(); + assert!(validate_for_export(&r).is_ok()); + } + + #[test] + fn article_missing_author_fails() { + let mut r = make_article(); + r.authors.clear(); + let err = validate_for_export(&r).unwrap_err(); + assert!( + matches!(err, BibtexError::MissingRequiredField { field, .. } if field == "author") + ); + } + + #[test] + fn article_missing_journal_fails() { + let mut r = make_article(); + r.fields.remove("journal"); + let err = validate_for_export(&r).unwrap_err(); + assert!( + matches!(err, BibtexError::MissingRequiredField { field, .. } if field == "journal") + ); + } + + #[test] + fn misc_has_no_required_fields() { + let r = Reference::new("anon", EntryType::Misc); + assert!(validate_for_export(&r).is_ok()); + } + + #[test] + fn phd_thesis_requires_school() { + let mut r = Reference::new("smith2020", EntryType::PhdThesis); + r.authors.push(Person::new("Smith")); + r.fields.insert("title".into(), "A Thesis".into()); + r.fields.insert("year".into(), "2020".into()); + let err = validate_for_export(&r).unwrap_err(); + assert!( + matches!(err, BibtexError::MissingRequiredField { field, .. } if field == "school") + ); + } +} diff --git a/brittle-core/src/bin/seed.rs b/brittle-core/src/bin/seed.rs new file mode 100644 index 0000000..a998eb9 --- /dev/null +++ b/brittle-core/src/bin/seed.rs @@ -0,0 +1,286 @@ +//! Creates an example Brittle repository with realistic academic references. +//! +//! For references that have freely available PDFs (arXiv preprints and open +//! author copies), the script downloads the PDF and attaches it to the +//! reference. Downloads that fail are skipped with a warning so the seed +//! always completes even without network access. +//! +//! Usage: +//! brittle-seed [PATH] +//! +//! PATH defaults to `~/brittle-example`. The directory must not already +//! contain a git repository. + +use std::io::Read; +use std::path::PathBuf; + +use brittle_core::{Brittle, EntryType, FsStore, Person, ReferenceId}; + +fn main() { + let path = match std::env::args().nth(1) { + Some(p) => PathBuf::from(p), + None => { + let home = std::env::var("HOME").expect("HOME not set"); + PathBuf::from(home).join("brittle-example") + } + }; + + if path.join(".git").exists() { + eprintln!("error: {} already contains a git repository", path.display()); + std::process::exit(1); + } + + std::fs::create_dir_all(&path).expect("could not create directory"); + + println!("Creating repository at {} …", path.display()); + let mut b = Brittle::create(&path).expect("create repository"); + + // ── Libraries ───────────────────────────────────────────────────────────── + + let cs = b.create_library("Computer Science", None).unwrap(); + let ml = b.create_library("Machine Learning", Some(cs.id)).unwrap(); + let sys = b.create_library("Systems", Some(cs.id)).unwrap(); + let math = b.create_library("Mathematics", None).unwrap(); + let pl = b.create_library("Programming Languages", Some(cs.id)).unwrap(); + + // ── References ──────────────────────────────────────────────────────────── + + // -- Machine Learning -- + + let mut r = b.create_reference("lecun1998gradient", EntryType::Article).unwrap(); + r.authors = vec![ + person("LeCun", "Yann"), + person("Bottou", "Léon"), + person("Bengio", "Yoshua"), + person("Haffner", "Patrick"), + ]; + r.fields.insert("title".into(), "Gradient-based learning applied to document recognition".into()); + r.fields.insert("journal".into(), "Proceedings of the IEEE".into()); + r.fields.insert("volume".into(), "86".into()); + r.fields.insert("number".into(), "11".into()); + r.fields.insert("pages".into(), "2278--2324".into()); + r.fields.insert("year".into(), "1998".into()); + let id = r.id; + b.update_reference(r).unwrap(); + b.add_to_library(ml.id, id).unwrap(); + attach_pdf(&mut b, id, "http://yann.lecun.com/exdb/publis/pdf/lecun-01a.pdf"); + + let mut r = b.create_reference("vaswani2017attention", EntryType::InProceedings).unwrap(); + r.authors = vec![ + person("Vaswani", "Ashish"), + person("Shazeer", "Noam"), + person("Parmar", "Niki"), + person("Uszkoreit", "Jakob"), + person("Jones", "Llion"), + person("Gomez", "Aidan N."), + person("Kaiser", "Łukasz"), + person("Polosukhin", "Illia"), + ]; + r.fields.insert("title".into(), "Attention Is All You Need".into()); + r.fields.insert("booktitle".into(), "Advances in Neural Information Processing Systems".into()); + r.fields.insert("volume".into(), "30".into()); + r.fields.insert("year".into(), "2017".into()); + let id = r.id; + b.update_reference(r).unwrap(); + b.add_to_library(ml.id, id).unwrap(); + attach_pdf(&mut b, id, "https://arxiv.org/pdf/1706.03762"); + + let mut r = b.create_reference("goodfellow2016deep", EntryType::Book).unwrap(); + r.authors = vec![ + person("Goodfellow", "Ian"), + person("Bengio", "Yoshua"), + person("Courville", "Aaron"), + ]; + r.fields.insert("title".into(), "Deep Learning".into()); + r.fields.insert("publisher".into(), "MIT Press".into()); + r.fields.insert("year".into(), "2016".into()); + r.fields.insert("url".into(), "http://www.deeplearningbook.org".into()); + let id = r.id; + b.update_reference(r).unwrap(); + b.add_to_library(ml.id, id).unwrap(); + // No freely available PDF for this book. + + let mut r = b.create_reference("ho2020denoising", EntryType::InProceedings).unwrap(); + r.authors = vec![ + person("Ho", "Jonathan"), + person("Jain", "Ajay"), + person("Abbeel", "Pieter"), + ]; + r.fields.insert("title".into(), "Denoising Diffusion Probabilistic Models".into()); + r.fields.insert("booktitle".into(), "Advances in Neural Information Processing Systems".into()); + r.fields.insert("volume".into(), "33".into()); + r.fields.insert("pages".into(), "6840--6851".into()); + r.fields.insert("year".into(), "2020".into()); + let id = r.id; + b.update_reference(r).unwrap(); + b.add_to_library(ml.id, id).unwrap(); + attach_pdf(&mut b, id, "https://arxiv.org/pdf/2006.11239"); + + // -- Systems -- + + let mut r = b.create_reference("lamport1978time", EntryType::Article).unwrap(); + r.authors = vec![person("Lamport", "Leslie")]; + r.fields.insert("title".into(), "Time, Clocks, and the Ordering of Events in a Distributed System".into()); + r.fields.insert("journal".into(), "Communications of the ACM".into()); + r.fields.insert("volume".into(), "21".into()); + r.fields.insert("number".into(), "7".into()); + r.fields.insert("pages".into(), "558--565".into()); + r.fields.insert("year".into(), "1978".into()); + let id = r.id; + b.update_reference(r).unwrap(); + b.add_to_library(sys.id, id).unwrap(); + attach_pdf(&mut b, id, "https://lamport.azurewebsites.net/pubs/time-clocks.pdf"); + + let mut r = b.create_reference("rosenblum1992lfs", EntryType::Article).unwrap(); + r.authors = vec![ + person("Rosenblum", "Mendel"), + person("Ousterhout", "John K."), + ]; + r.fields.insert("title".into(), "The Design and Implementation of a Log-Structured File System".into()); + r.fields.insert("journal".into(), "ACM Transactions on Computer Systems".into()); + r.fields.insert("volume".into(), "10".into()); + r.fields.insert("number".into(), "1".into()); + r.fields.insert("pages".into(), "26--52".into()); + r.fields.insert("year".into(), "1992".into()); + let id = r.id; + b.update_reference(r).unwrap(); + b.add_to_library(sys.id, id).unwrap(); + // Paywalled; no freely available PDF. + + let mut r = b.create_reference("dean2004mapreduce", EntryType::InProceedings).unwrap(); + r.authors = vec![ + person("Dean", "Jeffrey"), + person("Ghemawat", "Sanjay"), + ]; + r.fields.insert("title".into(), "MapReduce: Simplified Data Processing on Large Clusters".into()); + r.fields.insert("booktitle".into(), "OSDI".into()); + r.fields.insert("pages".into(), "137--150".into()); + r.fields.insert("year".into(), "2004".into()); + let id = r.id; + b.update_reference(r).unwrap(); + b.add_to_library(sys.id, id).unwrap(); + attach_pdf(&mut b, id, "https://static.googleusercontent.com/media/research.google.com/en//archive/mapreduce-osdi04.pdf"); + + // -- Programming Languages -- + + let mut r = b.create_reference("milner1978polymorphism", EntryType::Article).unwrap(); + r.authors = vec![person("Milner", "Robin")]; + r.fields.insert("title".into(), "A Theory of Type Polymorphism in Programming".into()); + r.fields.insert("journal".into(), "Journal of Computer and System Sciences".into()); + r.fields.insert("volume".into(), "17".into()); + r.fields.insert("number".into(), "3".into()); + r.fields.insert("pages".into(), "348--375".into()); + r.fields.insert("year".into(), "1978".into()); + let id = r.id; + b.update_reference(r).unwrap(); + b.add_to_library(pl.id, id).unwrap(); + // Paywalled; no freely available PDF. + + let mut r = b.create_reference("matsakis2014rust", EntryType::InProceedings).unwrap(); + r.authors = vec![ + person("Matsakis", "Nicholas D."), + person("Klock", "Felix S."), + ]; + r.fields.insert("title".into(), "The Rust Language".into()); + r.fields.insert("booktitle".into(), "ACM SIGAda Annual Conference on High Integrity Language Technology".into()); + r.fields.insert("pages".into(), "103--104".into()); + r.fields.insert("year".into(), "2014".into()); + let id = r.id; + b.update_reference(r).unwrap(); + b.add_to_library(pl.id, id).unwrap(); + // Paywalled; no freely available PDF. + + // -- Mathematics -- + + let mut r = b.create_reference("turing1936computable", EntryType::Article).unwrap(); + r.authors = vec![person("Turing", "Alan M.")]; + r.fields.insert("title".into(), "On Computable Numbers, with an Application to the Entscheidungsproblem".into()); + r.fields.insert("journal".into(), "Proceedings of the London Mathematical Society".into()); + r.fields.insert("volume".into(), "42".into()); + r.fields.insert("number".into(), "1".into()); + r.fields.insert("pages".into(), "230--265".into()); + r.fields.insert("year".into(), "1936".into()); + let id = r.id; + b.update_reference(r).unwrap(); + b.add_to_library(math.id, id).unwrap(); + // No freely available PDF. + + let mut r = b.create_reference("knuth1984texbook", EntryType::Book).unwrap(); + r.authors = vec![person("Knuth", "Donald E.")]; + r.fields.insert("title".into(), "The TeXbook".into()); + r.fields.insert("publisher".into(), "Addison-Wesley".into()); + r.fields.insert("year".into(), "1984".into()); + r.fields.insert("series".into(), "Computers and Typesetting".into()); + r.fields.insert("volume".into(), "A".into()); + let id = r.id; + b.update_reference(r).unwrap(); + b.add_to_library(math.id, id).unwrap(); + // Copyrighted book; no freely available PDF. + + // A reference in both ML and Mathematics (cross-library membership). + let mut r = b.create_reference("cybenko1989approximation", EntryType::Article).unwrap(); + r.authors = vec![person("Cybenko", "George")]; + r.fields.insert("title".into(), "Approximation by Superpositions of a Sigmoidal Function".into()); + r.fields.insert("journal".into(), "Mathematics of Control, Signals, and Systems".into()); + r.fields.insert("volume".into(), "2".into()); + r.fields.insert("number".into(), "4".into()); + r.fields.insert("pages".into(), "303--314".into()); + r.fields.insert("year".into(), "1989".into()); + let id = r.id; + b.update_reference(r).unwrap(); + b.add_to_library(ml.id, id).unwrap(); + b.add_to_library(math.id, id).unwrap(); + // Paywalled; no freely available PDF. + + println!(); + println!("Done."); + println!(); + println!(" Libraries : Computer Science (Machine Learning, Systems, Programming Languages), Mathematics"); + println!(" References: 12 across all libraries"); + println!(); + println!("Open the repository in Brittle with: :open {}", path.display()); +} + +// ── PDF download ────────────────────────────────────────────────────────────── + +/// Download the PDF at `url` and attach it to `id`. Prints progress and +/// skips silently on any error so the seed always completes. +fn attach_pdf(b: &mut Brittle, id: ReferenceId, url: &str) { + let label = url.rsplit('/').next().unwrap_or(url); + print!(" ↓ {label} … "); + std::io::Write::flush(&mut std::io::stdout()).ok(); + + match download(url) { + Err(e) => println!("skipped ({e})"), + Ok(bytes) => { + let tmp = std::env::temp_dir().join(format!("{id}.pdf")); + if let Err(e) = std::fs::write(&tmp, &bytes) { + println!("skipped (write: {e})"); + return; + } + match b.attach_pdf(id, &tmp) { + Ok(_) => println!("{} KB", bytes.len() / 1024), + Err(e) => println!("skipped (attach: {e})"), + } + let _ = std::fs::remove_file(&tmp); + } + } +} + +fn download(url: &str) -> Result, Box> { + let resp = ureq::get(url).call()?; + let mut buf = Vec::new(); + resp.into_reader().read_to_end(&mut buf)?; + Ok(buf) +} + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +fn person(family: &str, given: &str) -> Person { + Person { + family: family.into(), + given: Some(given.into()), + prefix: None, + suffix: None, + } +} diff --git a/brittle-core/src/error.rs b/brittle-core/src/error.rs new file mode 100644 index 0000000..bc5dd7f --- /dev/null +++ b/brittle-core/src/error.rs @@ -0,0 +1,104 @@ +use std::path::PathBuf; +use thiserror::Error; + +/// Top-level error returned from all public Brittle API methods. +#[derive(Debug, Error)] +pub enum BrittleError { + #[error("{0}")] + Store(#[from] StoreError), + + #[error("{0}")] + Validation(#[from] ValidationError), + + #[error("{0}")] + BibTeX(#[from] BibtexError), +} + +/// Errors from the storage layer. +#[derive(Debug, Error)] +pub enum StoreError { + #[error("{entity_type} not found: {id}")] + NotFound { entity_type: EntityType, id: String }, + + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + + #[error("serialization error: {message}")] + Serialization { message: String }, + + #[error("deserialization error for {path}: {message}")] + Deserialization { path: PathBuf, message: String }, + + #[error("git error: {0}")] + Git(#[from] git2::Error), + + #[error("repository not found at {path}")] + RepoNotFound { path: PathBuf }, + + #[error("repository already exists at {path}")] + RepoAlreadyExists { path: PathBuf }, +} + +/// The kind of entity involved in a not-found error. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EntityType { + Reference, + Library, + Annotation, + Snapshot, +} + +impl std::fmt::Display for EntityType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + EntityType::Reference => write!(f, "Reference"), + EntityType::Library => write!(f, "Library"), + EntityType::Annotation => write!(f, "Annotation"), + EntityType::Snapshot => write!(f, "Snapshot"), + } + } +} + +/// Business logic validation errors. +#[derive(Debug, Error)] +pub enum ValidationError { + #[error( + "library cycle detected: moving library {library_id} under {parent_id} would create a cycle" + )] + LibraryCycle { + library_id: String, + parent_id: String, + }, + + #[error("library {id} has children and cannot be deleted; delete or move children first")] + LibraryHasChildren { id: String }, + + #[error("cite key already exists: {cite_key}")] + DuplicateCiteKey { cite_key: String }, + + #[error("cite key cannot be empty")] + EmptyCiteKey, + + #[error("library name cannot be empty")] + EmptyLibraryName, + + #[error("reference {reference_id} has no PDF attached")] + NoPdfAttached { reference_id: String }, + + #[error("PDF file not found: {path}")] + PdfNotFound { path: PathBuf }, + + #[error("there are uncommitted changes; create a snapshot or call discard_changes() first")] + UncommittedChanges, +} + +/// Errors specific to BibTeX export. +#[derive(Debug, Error)] +pub enum BibtexError { + #[error("reference '{cite_key}' ({entry_type}): missing required field '{field}'")] + MissingRequiredField { + cite_key: String, + entry_type: String, + field: String, + }, +} diff --git a/brittle-core/src/lib.rs b/brittle-core/src/lib.rs new file mode 100644 index 0000000..4ffd5cc --- /dev/null +++ b/brittle-core/src/lib.rs @@ -0,0 +1,1051 @@ +//! `brittle-core` — the library crate for the Brittle literature management system. +//! +//! All operations go through the [`Brittle`] struct. The API is transport-agnostic: +//! every return type is plain data that any layer (UI, REST, CLI) can use directly. + +pub mod bibtex; +pub mod error; +pub mod model; +pub mod store; + +pub use error::{BibtexError, BrittleError, StoreError, ValidationError}; +pub use model::{ + Annotation, AnnotationId, AnnotationSet, AnnotationType, Color, EntryType, Library, LibraryId, + PdfAttachment, Person, Point, Quad, Rect, Reference, ReferenceId, Snapshot, TextMarkupType, +}; +pub use store::{FsStore, MemoryStore, Store}; + +use crate::bibtex::export_references; +use crate::error::EntityType; +use chrono::Utc; +use serde::Serialize; +use std::path::{Path, PathBuf}; + +/// A lightweight summary of a reference, suitable for list views. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct ReferenceSummary { + pub id: ReferenceId, + pub cite_key: String, + pub entry_type: EntryType, + pub title: Option, + pub authors: Vec, + pub year: Option, +} + +impl From<&Reference> for ReferenceSummary { + fn from(r: &Reference) -> Self { + Self { + id: r.id, + cite_key: r.cite_key.clone(), + entry_type: r.entry_type.clone(), + title: r.title().map(str::to_owned), + authors: r.authors.clone(), + year: r.year().map(str::to_owned), + } + } +} + +/// The main entry point for Brittle. +/// +/// Generic over [`Store`] to allow testing with [`MemoryStore`] and production +/// use with [`FsStore`] without duplicating business logic. +pub struct Brittle { + store: S, +} + +// ---- Constructors ---- + +impl Brittle { + /// Create a new Brittle repository at the given path. + pub fn create(path: &Path) -> Result { + let store = FsStore::create(path)?; + Ok(Self { store }) + } + + /// Open an existing Brittle repository. + pub fn open(path: &Path) -> Result { + let store = FsStore::open(path)?; + Ok(Self { store }) + } + + /// Returns the root path of the open repository. + pub fn repository_root(&self) -> &Path { + self.store.root() + } + + /// Return the absolute filesystem path to the PDF attached to a reference. + /// + /// Returns [`ValidationError::NoPdfAttached`] if the reference has no PDF. + pub fn get_pdf_path(&self, ref_id: ReferenceId) -> Result { + let reference = self.store.load_reference(ref_id)?; + reference + .pdf + .map(|att| self.store.root().join(&att.stored_path)) + .ok_or_else(|| { + ValidationError::NoPdfAttached { + reference_id: ref_id.to_string(), + } + .into() + }) + } + + /// Attach a PDF to a reference. + /// + /// Copies the file from `source_path` into the repository's `pdfs/` directory, + /// computes its SHA-256 content hash, and updates the reference record. + pub fn attach_pdf( + &mut self, + id: ReferenceId, + source_path: &Path, + ) -> Result { + use sha2::{Digest, Sha256}; + + if !source_path.exists() { + return Err(ValidationError::PdfNotFound { + path: source_path.to_owned(), + } + .into()); + } + + let mut reference = self.store.load_reference(id)?; + let dest_name = format!("{id}.pdf"); + let dest_path = self.store.pdf_dir().join(&dest_name); + let stored_path = Path::new("pdfs").join(&dest_name); + + let bytes = std::fs::read(source_path).map_err(StoreError::Io)?; + let mut hasher = Sha256::new(); + hasher.update(&bytes); + let hash = format!("{:x}", hasher.finalize()); + + std::fs::write(&dest_path, &bytes).map_err(StoreError::Io)?; + + let attachment = PdfAttachment { + stored_path, + content_hash: hash, + }; + reference.pdf = Some(attachment.clone()); + reference.modified_at = Utc::now(); + self.store.save_reference(&reference)?; + + Ok(attachment) + } +} + +impl Brittle { + /// Construct with an arbitrary store. Primarily for testing with [`MemoryStore`]. + pub fn with_store(store: S) -> Self { + Self { store } + } + + // ---- Search ---- + + /// Search all references. Returns summaries whose cite key, title, year, or + /// author family name contains `query` (case-insensitive). If `query` is + /// empty, returns all references. + pub fn search_references(&self, query: &str) -> Result, BrittleError> { + if query.is_empty() { + return self.list_references(); + } + let q = query.to_lowercase(); + let ids = self.store.list_reference_ids()?; + let mut results = Vec::new(); + for id in ids { + let r = self.store.load_reference(id)?; + if reference_matches(&r, &q) { + results.push(ReferenceSummary::from(&r)); + } + } + Ok(results) + } + + /// Search references within a specific library. + pub fn search_library_references( + &self, + library_id: LibraryId, + query: &str, + ) -> Result, BrittleError> { + if query.is_empty() { + return self.list_library_references(library_id); + } + let q = query.to_lowercase(); + let library = self.store.load_library(library_id)?; + let mut results = Vec::new(); + for ref_id in &library.members { + let r = self.store.load_reference(*ref_id)?; + if reference_matches(&r, &q) { + results.push(ReferenceSummary::from(&r)); + } + } + Ok(results) + } + + // ---- Reference CRUD ---- + + /// Create a new reference with the given cite key and entry type. + /// + /// Returns an error if the cite key is empty or already in use. + pub fn create_reference( + &mut self, + cite_key: impl Into, + entry_type: EntryType, + ) -> Result { + let cite_key = cite_key.into(); + if cite_key.is_empty() { + return Err(ValidationError::EmptyCiteKey.into()); + } + // Check for duplicate cite keys. + for id in self.store.list_reference_ids()? { + let existing = self.store.load_reference(id)?; + if existing.cite_key == cite_key { + return Err(ValidationError::DuplicateCiteKey { cite_key }.into()); + } + } + let reference = Reference::new(cite_key, entry_type); + self.store.save_reference(&reference)?; + Ok(reference) + } + + /// Get a reference by ID. + pub fn get_reference(&self, id: ReferenceId) -> Result { + Ok(self.store.load_reference(id)?) + } + + /// Replace the mutable fields of a reference. `id` and `created_at` are preserved. + /// `modified_at` is updated automatically. + pub fn update_reference( + &mut self, + mut reference: Reference, + ) -> Result { + // Ensure the reference exists. + let existing = self.store.load_reference(reference.id)?; + reference.created_at = existing.created_at; + reference.modified_at = Utc::now(); + + // Check that the (possibly changed) cite key doesn't conflict. + if reference.cite_key.is_empty() { + return Err(ValidationError::EmptyCiteKey.into()); + } + for id in self.store.list_reference_ids()? { + if id == reference.id { + continue; + } + let other = self.store.load_reference(id)?; + if other.cite_key == reference.cite_key { + return Err(ValidationError::DuplicateCiteKey { + cite_key: reference.cite_key, + } + .into()); + } + } + + self.store.save_reference(&reference)?; + Ok(reference) + } + + /// Delete a reference and cascade-remove all associated data: + /// library memberships and annotations. + pub fn delete_reference(&mut self, id: ReferenceId) -> Result<(), BrittleError> { + // Ensure it exists first. + self.store.load_reference(id)?; + + // Remove from all libraries. + for lib_id in self.store.list_library_ids()? { + let mut lib = self.store.load_library(lib_id)?; + if lib.members.remove(&id) { + lib.modified_at = Utc::now(); + self.store.save_library(&lib)?; + } + } + + // Delete annotations. + self.store.delete_annotations(id)?; + + // Delete the reference itself. + self.store.delete_reference(id)?; + + Ok(()) + } + + /// List all references as lightweight summaries. + pub fn list_references(&self) -> Result, BrittleError> { + let ids = self.store.list_reference_ids()?; + let mut summaries = Vec::with_capacity(ids.len()); + for id in ids { + let r = self.store.load_reference(id)?; + summaries.push(ReferenceSummary::from(&r)); + } + Ok(summaries) + } + + /// Set a single field on a reference. Use `"title"`, `"year"`, `"journal"`, etc. + pub fn set_field( + &mut self, + id: ReferenceId, + field: &str, + value: impl Into, + ) -> Result<(), BrittleError> { + let mut reference = self.store.load_reference(id)?; + reference.fields.insert(field.to_owned(), value.into()); + reference.modified_at = Utc::now(); + self.store.save_reference(&reference)?; + Ok(()) + } + + /// Remove a field from a reference. + pub fn remove_field(&mut self, id: ReferenceId, field: &str) -> Result<(), BrittleError> { + let mut reference = self.store.load_reference(id)?; + reference.fields.remove(field); + reference.modified_at = Utc::now(); + self.store.save_reference(&reference)?; + Ok(()) + } + + // ---- Library CRUD ---- + + /// Create a new library. + pub fn create_library( + &mut self, + name: impl Into, + parent_id: Option, + ) -> Result { + let name = name.into(); + if name.is_empty() { + return Err(ValidationError::EmptyLibraryName.into()); + } + // Verify parent exists if provided. + if let Some(pid) = parent_id { + self.store.load_library(pid)?; + } + let library = Library::new(name, parent_id); + self.store.save_library(&library)?; + Ok(library) + } + + /// Get a library by ID. + pub fn get_library(&self, id: LibraryId) -> Result { + Ok(self.store.load_library(id)?) + } + + /// Rename a library. + pub fn rename_library( + &mut self, + id: LibraryId, + new_name: impl Into, + ) -> Result { + let new_name = new_name.into(); + if new_name.is_empty() { + return Err(ValidationError::EmptyLibraryName.into()); + } + let mut library = self.store.load_library(id)?; + library.name = new_name; + library.modified_at = Utc::now(); + self.store.save_library(&library)?; + Ok(library) + } + + /// Move a library to a new parent. Validates that no cycle is created. + pub fn move_library( + &mut self, + id: LibraryId, + new_parent: Option, + ) -> Result { + // Verify new parent exists if provided. + if let Some(pid) = new_parent { + self.store.load_library(pid)?; + } + + // Cycle detection: walk from new_parent up to root. + // If we encounter `id` in the ancestry chain, moving would create a cycle. + if let Some(pid) = new_parent { + let mut current = pid; + loop { + if current == id { + return Err(ValidationError::LibraryCycle { + library_id: id.to_string(), + parent_id: pid.to_string(), + } + .into()); + } + let lib = self.store.load_library(current).map_err(|_| { + // Broken ancestor chain — treat as no cycle. + BrittleError::Validation(ValidationError::LibraryCycle { + library_id: id.to_string(), + parent_id: pid.to_string(), + }) + }); + match lib { + Ok(l) => match l.parent_id { + Some(next) => current = next, + None => break, + }, + Err(_) => break, + } + } + } + + let mut library = self.store.load_library(id)?; + library.parent_id = new_parent; + library.modified_at = Utc::now(); + self.store.save_library(&library)?; + Ok(library) + } + + /// Delete a library. Fails if it has children. References are disassociated, not deleted. + pub fn delete_library(&mut self, id: LibraryId) -> Result<(), BrittleError> { + // Ensure it exists. + self.store.load_library(id)?; + + // Reject if any library has this one as its parent. + for lib_id in self.store.list_library_ids()? { + if lib_id == id { + continue; + } + let child = self.store.load_library(lib_id)?; + if child.parent_id == Some(id) { + return Err(ValidationError::LibraryHasChildren { id: id.to_string() }.into()); + } + } + + self.store.delete_library(id)?; + Ok(()) + } + + /// List all root libraries (those with no parent). + pub fn list_root_libraries(&self) -> Result, BrittleError> { + self.list_libraries_where(|lib| lib.parent_id.is_none()) + } + + /// List direct children of a library. + pub fn list_child_libraries(&self, parent_id: LibraryId) -> Result, BrittleError> { + // Ensure parent exists. + self.store.load_library(parent_id)?; + self.list_libraries_where(|lib| lib.parent_id == Some(parent_id)) + } + + fn list_libraries_where( + &self, + predicate: impl Fn(&Library) -> bool, + ) -> Result, BrittleError> { + let ids = self.store.list_library_ids()?; + let mut libs = Vec::new(); + for id in ids { + let lib = self.store.load_library(id)?; + if predicate(&lib) { + libs.push(lib); + } + } + Ok(libs) + } + + // ---- Library Membership ---- + + /// Add a reference to a library. + pub fn add_to_library( + &mut self, + library_id: LibraryId, + reference_id: ReferenceId, + ) -> Result<(), BrittleError> { + // Verify both exist. + self.store.load_reference(reference_id)?; + let mut library = self.store.load_library(library_id)?; + library.members.insert(reference_id); + library.modified_at = Utc::now(); + self.store.save_library(&library)?; + Ok(()) + } + + /// Remove a reference from a library. + pub fn remove_from_library( + &mut self, + library_id: LibraryId, + reference_id: ReferenceId, + ) -> Result<(), BrittleError> { + let mut library = self.store.load_library(library_id)?; + library.members.remove(&reference_id); + library.modified_at = Utc::now(); + self.store.save_library(&library)?; + Ok(()) + } + + /// List all references directly in a library as lightweight summaries. + pub fn list_library_references( + &self, + library_id: LibraryId, + ) -> Result, BrittleError> { + let library = self.store.load_library(library_id)?; + let mut summaries = Vec::new(); + for ref_id in &library.members { + let r = self.store.load_reference(*ref_id)?; + summaries.push(ReferenceSummary::from(&r)); + } + Ok(summaries) + } + + /// List all references in a library and all its descendant libraries. + /// + /// De-duplicates references that appear in multiple sub-libraries. + pub fn list_library_references_recursive( + &self, + library_id: LibraryId, + ) -> Result, BrittleError> { + use std::collections::BTreeSet; + let mut ref_ids: BTreeSet = BTreeSet::new(); + let mut queue = vec![library_id]; + while let Some(id) = queue.pop() { + let lib = self.store.load_library(id)?; + ref_ids.extend(lib.members.iter().copied()); + let children = self.list_child_libraries(id)?; + queue.extend(children.into_iter().map(|c| c.id)); + } + let mut summaries = Vec::new(); + for ref_id in ref_ids { + summaries.push(ReferenceSummary::from(&self.store.load_reference(ref_id)?)); + } + Ok(summaries) + } + + /// List all libraries that contain a given reference. + pub fn list_reference_libraries( + &self, + reference_id: ReferenceId, + ) -> Result, BrittleError> { + // Ensure the reference exists. + self.store.load_reference(reference_id)?; + self.list_libraries_where(|lib| lib.members.contains(&reference_id)) + } + + // ---- Library ancestry & cascade delete ---- + + /// Return the ancestor chain of a library, ordered from root down to its + /// direct parent. + /// + /// Returns an empty `Vec` for root libraries (those with no parent). + /// + /// Example: given `Root > Science > Physics`, calling this on `Physics` + /// returns `[Root, Science]`. + pub fn get_library_ancestors(&self, id: LibraryId) -> Result, BrittleError> { + let mut lib = self.store.load_library(id)?; + let mut ancestors = Vec::new(); + while let Some(parent_id) = lib.parent_id { + let parent = self.store.load_library(parent_id)?; + ancestors.push(parent.clone()); + lib = parent; + } + ancestors.reverse(); + Ok(ancestors) + } + + /// Delete a library and **all its descendants**, disassociating member + /// references (references are not deleted, only removed from the deleted + /// libraries). + /// + /// Use after confirming with the user. For the safe, rejection-on-children + /// variant see [`delete_library`]. + pub fn force_delete_library(&mut self, id: LibraryId) -> Result<(), BrittleError> { + // Ensure the target exists before doing anything. + self.store.load_library(id)?; + + // Collect the target and all its descendants via BFS. + let mut to_delete = vec![id]; + let mut i = 0; + while i < to_delete.len() { + let current = to_delete[i]; + for lib_id in self.store.list_library_ids()? { + if to_delete.contains(&lib_id) { + continue; + } + let lib = self.store.load_library(lib_id)?; + if lib.parent_id == Some(current) { + to_delete.push(lib_id); + } + } + i += 1; + } + + // Library records hold membership; deleting the record removes + // memberships for free. References themselves are unaffected. + for lib_id in to_delete { + self.store.delete_library(lib_id)?; + } + Ok(()) + } + + // ---- BibTeX Export ---- + + /// Export a set of references as a BibTeX string. + /// Returns an error if any referenced ID is not found. + /// References with missing required fields are skipped; their errors are collected. + pub fn export_bibtex( + &self, + reference_ids: &[ReferenceId], + ) -> Result<(String, Vec), BrittleError> { + let mut refs = Vec::with_capacity(reference_ids.len()); + for &id in reference_ids { + refs.push(self.store.load_reference(id)?); + } + Ok(export_references(&refs)) + } + + /// Export all references in a library as a BibTeX string. + pub fn export_library_bibtex( + &self, + library_id: LibraryId, + ) -> Result<(String, Vec), BrittleError> { + let library = self.store.load_library(library_id)?; + let ids: Vec<_> = library.members.iter().copied().collect(); + self.export_bibtex(&ids) + } + + // ---- Annotations ---- + + /// Add an annotation to a reference's PDF. + pub fn create_annotation( + &mut self, + reference_id: ReferenceId, + page: u32, + annotation_type: AnnotationType, + content: Option, + ) -> Result { + // Ensure the reference exists. + self.store.load_reference(reference_id)?; + + let mut set = self.store.load_annotations(reference_id)?; + let mut ann = Annotation::new(reference_id, page, annotation_type); + ann.content = content; + set.annotations.push(ann.clone()); + self.store.save_annotations(&set)?; + Ok(ann) + } + + /// Get all annotations for a reference. + pub fn get_annotations( + &self, + reference_id: ReferenceId, + ) -> Result, BrittleError> { + Ok(self.store.load_annotations(reference_id)?.annotations) + } + + /// Update an annotation. Matches by `annotation.id`; fails if not found. + pub fn update_annotation( + &mut self, + mut annotation: Annotation, + ) -> Result { + let ref_id = annotation.reference_id; + let mut set = self.store.load_annotations(ref_id)?; + + let slot = set + .annotations + .iter_mut() + .find(|a| a.id == annotation.id) + .ok_or_else(|| { + BrittleError::Store(StoreError::NotFound { + entity_type: EntityType::Annotation, + id: annotation.id.to_string(), + }) + })?; + + annotation.created_at = slot.created_at; + annotation.modified_at = Utc::now(); + *slot = annotation.clone(); + self.store.save_annotations(&set)?; + Ok(annotation) + } + + /// Delete an annotation by ID. + pub fn delete_annotation( + &mut self, + reference_id: ReferenceId, + annotation_id: AnnotationId, + ) -> Result<(), BrittleError> { + let mut set = self.store.load_annotations(reference_id)?; + let before = set.annotations.len(); + set.annotations.retain(|a| a.id != annotation_id); + if set.annotations.len() == before { + return Err(StoreError::NotFound { + entity_type: EntityType::Annotation, + id: annotation_id.to_string(), + } + .into()); + } + self.store.save_annotations(&set)?; + Ok(()) + } + + // ---- Snapshots ---- + + /// Create a named snapshot of the current state. + pub fn create_snapshot(&mut self, message: &str) -> Result { + Ok(self.store.create_snapshot(message)?) + } + + /// List all snapshots in reverse chronological order (most recent first). + pub fn list_snapshots(&self) -> Result, BrittleError> { + Ok(self.store.list_snapshots()?) + } + + /// Restore to a previous snapshot. + /// + /// Returns an error if there are uncommitted changes. Use [`discard_changes`] first. + pub fn restore_snapshot(&mut self, snapshot_id: &str) -> Result<(), BrittleError> { + if self.store.has_uncommitted_changes()? { + return Err(ValidationError::UncommittedChanges.into()); + } + Ok(self.store.restore_snapshot(snapshot_id)?) + } + + /// Returns `true` if there are unsaved changes not yet captured in a snapshot. + pub fn has_uncommitted_changes(&self) -> Result { + Ok(self.store.has_uncommitted_changes()?) + } + + /// Discard all uncommitted changes, reverting to the state of the last snapshot. + pub fn discard_changes(&mut self) -> Result<(), BrittleError> { + let snapshots = self.store.list_snapshots()?; + if let Some(latest) = snapshots.into_iter().next() { + self.store.restore_snapshot(&latest.id)?; + } + Ok(()) + } +} + +/// Case-insensitive substring match across key reference fields. +fn reference_matches(r: &Reference, query: &str) -> bool { + r.cite_key.to_lowercase().contains(query) + || r.title().is_some_and(|t| t.to_lowercase().contains(query)) + || r.year().is_some_and(|y| y.to_lowercase().contains(query)) + || r.authors + .iter() + .any(|a| a.family.to_lowercase().contains(query)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::model::{AnnotationType, Color, TextMarkupType}; + + fn make_brittle() -> Brittle { + Brittle::with_store(MemoryStore::new()) + } + + // ---- Reference tests ---- + + #[test] + fn create_and_get_reference() { + let mut b = make_brittle(); + let r = b.create_reference("doe2024", EntryType::Article).unwrap(); + let fetched = b.get_reference(r.id).unwrap(); + assert_eq!(fetched.cite_key, "doe2024"); + } + + #[test] + fn empty_cite_key_rejected() { + let mut b = make_brittle(); + assert!(matches!( + b.create_reference("", EntryType::Misc).unwrap_err(), + BrittleError::Validation(ValidationError::EmptyCiteKey) + )); + } + + #[test] + fn duplicate_cite_key_rejected() { + let mut b = make_brittle(); + b.create_reference("same", EntryType::Article).unwrap(); + assert!(matches!( + b.create_reference("same", EntryType::Book).unwrap_err(), + BrittleError::Validation(ValidationError::DuplicateCiteKey { .. }) + )); + } + + #[test] + fn delete_reference_cascades_to_libraries_and_annotations() { + let mut b = make_brittle(); + let r = b.create_reference("cascade2024", EntryType::Misc).unwrap(); + let lib = b.create_library("Test Lib", None).unwrap(); + b.add_to_library(lib.id, r.id).unwrap(); + + // Create annotation. + b.create_annotation( + r.id, + 0, + AnnotationType::Note { + position: Point { x: 0.0, y: 0.0 }, + }, + None, + ) + .unwrap(); + + b.delete_reference(r.id).unwrap(); + + // Reference gone. + assert!(b.get_reference(r.id).is_err()); + + // Library no longer contains the reference. + let lib2 = b.get_library(lib.id).unwrap(); + assert!(!lib2.members.contains(&r.id)); + + // Annotations gone. + let anns = b.get_annotations(r.id).unwrap(); + assert!(anns.is_empty()); + } + + #[test] + fn set_and_remove_field() { + let mut b = make_brittle(); + let r = b + .create_reference("fields2024", EntryType::Article) + .unwrap(); + b.set_field(r.id, "title", "Great Paper").unwrap(); + let r2 = b.get_reference(r.id).unwrap(); + assert_eq!(r2.fields.get("title").unwrap(), "Great Paper"); + + b.remove_field(r.id, "title").unwrap(); + let r3 = b.get_reference(r.id).unwrap(); + assert!(!r3.fields.contains_key("title")); + } + + // ---- Library tests ---- + + #[test] + fn create_and_list_root_libraries() { + let mut b = make_brittle(); + b.create_library("Science", None).unwrap(); + b.create_library("Arts", None).unwrap(); + let roots = b.list_root_libraries().unwrap(); + assert_eq!(roots.len(), 2); + } + + #[test] + fn empty_library_name_rejected() { + let mut b = make_brittle(); + assert!(matches!( + b.create_library("", None).unwrap_err(), + BrittleError::Validation(ValidationError::EmptyLibraryName) + )); + } + + #[test] + fn nested_libraries_and_children() { + let mut b = make_brittle(); + let parent = b.create_library("Science", None).unwrap(); + b.create_library("Physics", Some(parent.id)).unwrap(); + b.create_library("Biology", Some(parent.id)).unwrap(); + let children = b.list_child_libraries(parent.id).unwrap(); + assert_eq!(children.len(), 2); + } + + #[test] + fn delete_library_with_children_fails() { + let mut b = make_brittle(); + let parent = b.create_library("Parent", None).unwrap(); + b.create_library("Child", Some(parent.id)).unwrap(); + assert!(matches!( + b.delete_library(parent.id).unwrap_err(), + BrittleError::Validation(ValidationError::LibraryHasChildren { .. }) + )); + } + + #[test] + fn move_library_cycle_detection() { + let mut b = make_brittle(); + let a = b.create_library("A", None).unwrap(); + let b_lib = b.create_library("B", Some(a.id)).unwrap(); + // Moving A under B would create A -> B -> A cycle. + assert!(matches!( + b.move_library(a.id, Some(b_lib.id)).unwrap_err(), + BrittleError::Validation(ValidationError::LibraryCycle { .. }) + )); + } + + // ---- Membership tests ---- + + #[test] + fn multi_membership() { + let mut b = make_brittle(); + let r = b.create_reference("multi2024", EntryType::Article).unwrap(); + let lib1 = b.create_library("Lib 1", None).unwrap(); + let lib2 = b.create_library("Lib 2", None).unwrap(); + + b.add_to_library(lib1.id, r.id).unwrap(); + b.add_to_library(lib2.id, r.id).unwrap(); + + let containing = b.list_reference_libraries(r.id).unwrap(); + let ids: Vec<_> = containing.iter().map(|l| l.id).collect(); + assert!(ids.contains(&lib1.id)); + assert!(ids.contains(&lib2.id)); + } + + #[test] + fn list_library_references() { + let mut b = make_brittle(); + let r1 = b.create_reference("r1", EntryType::Misc).unwrap(); + let r2 = b.create_reference("r2", EntryType::Misc).unwrap(); + let lib = b.create_library("All", None).unwrap(); + b.add_to_library(lib.id, r1.id).unwrap(); + b.add_to_library(lib.id, r2.id).unwrap(); + + let summaries = b.list_library_references(lib.id).unwrap(); + assert_eq!(summaries.len(), 2); + } + + // ---- BibTeX export tests ---- + + #[test] + fn export_bibtex_valid_reference() { + let mut b = make_brittle(); + let mut r = b + .create_reference("turing1950", EntryType::Article) + .unwrap(); + r.authors.push(Person::new("Turing")); + r.fields + .insert("title".into(), "Computing Machinery".into()); + r.fields.insert("journal".into(), "Mind".into()); + r.fields.insert("year".into(), "1950".into()); + b.update_reference(r.clone()).unwrap(); + + let (bibtex, errors) = b.export_bibtex(&[r.id]).unwrap(); + assert!(errors.is_empty()); + assert!(bibtex.contains("@article{turing1950,")); + } + + // ---- Annotation tests ---- + + #[test] + fn create_update_delete_annotation() { + let mut b = make_brittle(); + let r = b.create_reference("ann2024", EntryType::Misc).unwrap(); + + let ann = b + .create_annotation( + r.id, + 0, + AnnotationType::TextMarkup { + markup_type: TextMarkupType::Highlight, + quads: vec![], + color: Color::YELLOW, + selected_text: Some("key text".into()), + }, + Some("Important!".into()), + ) + .unwrap(); + + let anns = b.get_annotations(r.id).unwrap(); + assert_eq!(anns.len(), 1); + assert_eq!(anns[0].content.as_deref(), Some("Important!")); + + // Update content. + let mut updated = ann.clone(); + updated.content = Some("Very important!".into()); + b.update_annotation(updated).unwrap(); + let anns2 = b.get_annotations(r.id).unwrap(); + assert_eq!(anns2[0].content.as_deref(), Some("Very important!")); + + // Delete. + b.delete_annotation(r.id, ann.id).unwrap(); + assert!(b.get_annotations(r.id).unwrap().is_empty()); + } + + // ---- Library ancestors tests ---- + + #[test] + fn ancestors_of_root_library_is_empty() { + let mut b = make_brittle(); + let root = b.create_library("Root", None).unwrap(); + assert!(b.get_library_ancestors(root.id).unwrap().is_empty()); + } + + #[test] + fn ancestors_of_child_returns_parent() { + let mut b = make_brittle(); + let root = b.create_library("Root", None).unwrap(); + let child = b.create_library("Child", Some(root.id)).unwrap(); + let ancestors = b.get_library_ancestors(child.id).unwrap(); + assert_eq!(ancestors.len(), 1); + assert_eq!(ancestors[0].id, root.id); + } + + #[test] + fn ancestors_are_ordered_root_first() { + let mut b = make_brittle(); + let root = b.create_library("Root", None).unwrap(); + let mid = b.create_library("Mid", Some(root.id)).unwrap(); + let leaf = b.create_library("Leaf", Some(mid.id)).unwrap(); + + let ancestors = b.get_library_ancestors(leaf.id).unwrap(); + assert_eq!(ancestors.len(), 2); + assert_eq!(ancestors[0].id, root.id); + assert_eq!(ancestors[1].id, mid.id); + } + + #[test] + fn ancestors_of_nonexistent_library_returns_error() { + let b = make_brittle(); + let fake_id = LibraryId::new(); + assert!(b.get_library_ancestors(fake_id).is_err()); + } + + // ---- Force-delete library tests ---- + + #[test] + fn force_delete_removes_library_with_children() { + let mut b = make_brittle(); + let parent = b.create_library("Parent", None).unwrap(); + let child = b.create_library("Child", Some(parent.id)).unwrap(); + + b.force_delete_library(parent.id).unwrap(); + + assert!(b.get_library(parent.id).is_err()); + assert!(b.get_library(child.id).is_err()); + } + + #[test] + fn force_delete_removes_entire_subtree() { + let mut b = make_brittle(); + let root = b.create_library("Root", None).unwrap(); + let a = b.create_library("A", Some(root.id)).unwrap(); + let b_lib = b.create_library("B", Some(root.id)).unwrap(); + let a1 = b.create_library("A1", Some(a.id)).unwrap(); + + b.force_delete_library(root.id).unwrap(); + + for id in [root.id, a.id, b_lib.id, a1.id] { + assert!(b.get_library(id).is_err()); + } + } + + #[test] + fn force_delete_leaves_references_intact() { + let mut b = make_brittle(); + let lib = b.create_library("Lib", None).unwrap(); + let r = b.create_reference("keep2024", EntryType::Misc).unwrap(); + b.add_to_library(lib.id, r.id).unwrap(); + + b.force_delete_library(lib.id).unwrap(); + + // Library is gone; reference survives. + assert!(b.get_library(lib.id).is_err()); + assert!(b.get_reference(r.id).is_ok()); + } + + #[test] + fn force_delete_nonexistent_library_returns_error() { + let mut b = make_brittle(); + let fake_id = LibraryId::new(); + assert!(b.force_delete_library(fake_id).is_err()); + } + + // ---- Snapshot tests ---- + + #[test] + fn snapshot_workflow() { + let mut b = make_brittle(); + let r = b.create_reference("snap2024", EntryType::Misc).unwrap(); + let snap = b.create_snapshot("baseline").unwrap(); + + b.delete_reference(r.id).unwrap(); + assert!(b.get_reference(r.id).is_err()); + + // MemoryStore allows restore without uncommitted-changes check. + b.store.restore_snapshot(&snap.id).unwrap(); + assert!(b.get_reference(r.id).is_ok()); + } +} diff --git a/brittle-core/src/model/annotation.rs b/brittle-core/src/model/annotation.rs new file mode 100644 index 0000000..ceea416 --- /dev/null +++ b/brittle-core/src/model/annotation.rs @@ -0,0 +1,229 @@ +use crate::model::ids::{AnnotationId, ReferenceId}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +/// A point in PDF coordinate space. +/// Origin is bottom-left; units are points (1/72 inch), matching ISO 32000. +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct Point { + pub x: f64, + pub y: f64, +} + +/// A rectangle in PDF coordinate space. +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct Rect { + pub x: f64, + pub y: f64, + pub width: f64, + pub height: f64, +} + +/// A quadrilateral for text markup annotations (highlight, underline, etc.). +/// Four points define one region, typically one line of text. +/// Matches the PDF spec QuadPoints representation (4 vertices per quad). +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct Quad { + pub points: [Point; 4], +} + +/// RGBA color. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct Color { + pub r: u8, + pub g: u8, + pub b: u8, + pub a: u8, +} + +impl Color { + pub const YELLOW: Color = Color { + r: 255, + g: 255, + b: 0, + a: 128, + }; + pub const RED: Color = Color { + r: 255, + g: 0, + b: 0, + a: 128, + }; + pub const GREEN: Color = Color { + r: 0, + g: 255, + b: 0, + a: 128, + }; +} + +/// The four text markup annotation types defined in ISO 32000. +/// All share the same QuadPoints-based geometry. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum TextMarkupType { + Highlight, + Underline, + Squiggly, + StrikeOut, +} + +/// The kind of annotation and its type-specific geometry/data. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "lowercase")] +pub enum AnnotationType { + /// Text markup (highlight, underline, squiggly, strikeout). + /// Uses QuadPoints per PDF spec for precise multi-line region selection. + TextMarkup { + markup_type: TextMarkupType, + quads: Vec, + color: Color, + /// The selected text, stored for search and export without re-reading the PDF. + selected_text: Option, + }, + /// Sticky note (popup comment). + Note { position: Point }, + /// Inline text box. + FreeText { rect: Rect }, + /// Freehand ink drawing (e.g., circling a diagram). + Ink { + /// Multiple strokes, each a sequence of connected points. + paths: Vec>, + color: Color, + /// Stroke width in points. + width: f64, + }, + /// Area/image selection for extracting figures from PDFs. + Area { rect: Rect }, +} + +/// A single annotation on a PDF page. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Annotation { + pub id: AnnotationId, + pub reference_id: ReferenceId, + /// 0-indexed physical page number. + pub page: u32, + /// Display page label (e.g., "iv", "23") — may differ from the physical page index. + pub page_label: Option, + pub annotation_type: AnnotationType, + /// Free-form text: note body, comment on a highlight, etc. + pub content: Option, + pub created_at: DateTime, + pub modified_at: DateTime, +} + +impl Annotation { + pub fn new(reference_id: ReferenceId, page: u32, annotation_type: AnnotationType) -> Self { + let now = Utc::now(); + Self { + id: AnnotationId::new(), + reference_id, + page, + page_label: None, + annotation_type, + content: None, + created_at: now, + modified_at: now, + } + } +} + +/// All annotations for a single reference, stored as one file. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct AnnotationSet { + pub reference_id: ReferenceId, + pub annotations: Vec, +} + +impl AnnotationSet { + pub fn new(reference_id: ReferenceId) -> Self { + Self { + reference_id, + annotations: Vec::new(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_highlight() -> AnnotationType { + AnnotationType::TextMarkup { + markup_type: TextMarkupType::Highlight, + quads: vec![Quad { + points: [ + Point { x: 10.0, y: 20.0 }, + Point { x: 100.0, y: 20.0 }, + Point { x: 10.0, y: 30.0 }, + Point { x: 100.0, y: 30.0 }, + ], + }], + color: Color::YELLOW, + selected_text: Some("important text".into()), + } + } + + #[test] + fn annotation_serde_round_trip_highlight() { + let ref_id = ReferenceId::new(); + let set = AnnotationSet { + reference_id: ref_id, + annotations: vec![Annotation::new(ref_id, 3, make_highlight())], + }; + + let toml_str = toml::to_string(&set).expect("serialize"); + let set2: AnnotationSet = toml::from_str(&toml_str).expect("deserialize"); + + assert_eq!(set.reference_id, set2.reference_id); + assert_eq!(set.annotations.len(), set2.annotations.len()); + assert_eq!(set.annotations[0].page, set2.annotations[0].page); + } + + #[test] + fn annotation_serde_round_trip_ink() { + let ref_id = ReferenceId::new(); + let ink = AnnotationType::Ink { + paths: vec![vec![Point { x: 0.0, y: 0.0 }, Point { x: 10.0, y: 10.0 }]], + color: Color::RED, + width: 2.0, + }; + let set = AnnotationSet { + reference_id: ref_id, + annotations: vec![Annotation::new(ref_id, 0, ink)], + }; + + let toml_str = toml::to_string(&set).expect("serialize"); + let set2: AnnotationSet = toml::from_str(&toml_str).expect("deserialize"); + assert_eq!(set, set2); + } + + #[test] + fn all_markup_types_serialize() { + let ref_id = ReferenceId::new(); + for markup_type in [ + TextMarkupType::Highlight, + TextMarkupType::Underline, + TextMarkupType::Squiggly, + TextMarkupType::StrikeOut, + ] { + let ann = Annotation::new( + ref_id, + 0, + AnnotationType::TextMarkup { + markup_type, + quads: vec![], + color: Color::GREEN, + selected_text: None, + }, + ); + let set = AnnotationSet { + reference_id: ref_id, + annotations: vec![ann], + }; + let toml_str = toml::to_string(&set).expect("serialize"); + let _: AnnotationSet = toml::from_str(&toml_str).expect("deserialize"); + } + } +} diff --git a/brittle-core/src/model/ids.rs b/brittle-core/src/model/ids.rs new file mode 100644 index 0000000..6e052fb --- /dev/null +++ b/brittle-core/src/model/ids.rs @@ -0,0 +1,67 @@ +use serde::{Deserialize, Serialize}; +use std::fmt; +use uuid::Uuid; + +macro_rules! define_id { + ($name:ident) => { + #[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, + )] + pub struct $name(pub Uuid); + + impl $name { + pub fn new() -> Self { + Self(Uuid::now_v7()) + } + } + + impl Default for $name { + fn default() -> Self { + Self::new() + } + } + + impl From for $name { + fn from(uuid: Uuid) -> Self { + Self(uuid) + } + } + + impl fmt::Display for $name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } + } + }; +} + +define_id!(ReferenceId); +define_id!(LibraryId); +define_id!(AnnotationId); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_generates_unique_ids() { + let a = ReferenceId::new(); + let b = ReferenceId::new(); + assert_ne!(a, b); + } + + #[test] + fn display_is_uuid_format() { + let id = ReferenceId::new(); + let s = id.to_string(); + assert_eq!(s.len(), 36); // UUID hyphenated format + } + + #[test] + fn serde_round_trip() { + let id = LibraryId::new(); + let json = serde_json::to_string(&id).unwrap(); + let id2: LibraryId = serde_json::from_str(&json).unwrap(); + assert_eq!(id, id2); + } +} diff --git a/brittle-core/src/model/library.rs b/brittle-core/src/model/library.rs new file mode 100644 index 0000000..9f544be --- /dev/null +++ b/brittle-core/src/model/library.rs @@ -0,0 +1,66 @@ +use crate::model::ids::{LibraryId, ReferenceId}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeSet; + +/// A named collection of references. Forms a tree via `parent_id`. +/// +/// References are not "owned" by a library — they exist in a flat pool. +/// A reference can appear in multiple libraries (multi-membership). +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Library { + pub id: LibraryId, + pub name: String, + /// `None` means this is a root library (no parent). + pub parent_id: Option, + /// The set of references that are members of this library. + /// BTreeSet for deterministic serialization order. + pub members: BTreeSet, + pub created_at: DateTime, + pub modified_at: DateTime, +} + +impl Library { + pub fn new(name: impl Into, parent_id: Option) -> Self { + let now = Utc::now(); + Self { + id: LibraryId::new(), + name: name.into(), + parent_id, + members: BTreeSet::new(), + created_at: now, + modified_at: now, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn library_serde_round_trip() { + let mut lib = Library::new("Machine Learning", None); + let ref_id = ReferenceId::new(); + lib.members.insert(ref_id); + + let toml_str = toml::to_string(&lib).expect("serialize to TOML"); + let lib2: Library = toml::from_str(&toml_str).expect("deserialize from TOML"); + + assert_eq!(lib.id, lib2.id); + assert_eq!(lib.name, lib2.name); + assert_eq!(lib.members, lib2.members); + assert_eq!(lib.parent_id, lib2.parent_id); + } + + #[test] + fn nested_library_serde_round_trip() { + let parent = Library::new("Science", None); + let child = Library::new("Physics", Some(parent.id)); + + let toml_str = toml::to_string(&child).expect("serialize to TOML"); + let child2: Library = toml::from_str(&toml_str).expect("deserialize from TOML"); + + assert_eq!(child2.parent_id, Some(parent.id)); + } +} diff --git a/brittle-core/src/model/mod.rs b/brittle-core/src/model/mod.rs new file mode 100644 index 0000000..9a54225 --- /dev/null +++ b/brittle-core/src/model/mod.rs @@ -0,0 +1,13 @@ +pub mod annotation; +pub mod ids; +pub mod library; +pub mod reference; +pub mod snapshot; + +pub use annotation::{ + Annotation, AnnotationSet, AnnotationType, Color, Point, Quad, Rect, TextMarkupType, +}; +pub use ids::{AnnotationId, LibraryId, ReferenceId}; +pub use library::Library; +pub use reference::{EntryType, PdfAttachment, Person, Reference}; +pub use snapshot::Snapshot; diff --git a/brittle-core/src/model/reference.rs b/brittle-core/src/model/reference.rs new file mode 100644 index 0000000..1341eb2 --- /dev/null +++ b/brittle-core/src/model/reference.rs @@ -0,0 +1,241 @@ +use crate::model::ids::ReferenceId; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::fmt; +use std::path::PathBuf; + +/// A person (author, editor, translator, etc.). +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Person { + pub family: String, + pub given: Option, + /// Name prefix: "von", "de", "van der", etc. + pub prefix: Option, + /// Name suffix: "Jr.", "III", etc. + pub suffix: Option, +} + +impl Person { + pub fn new(family: impl Into) -> Self { + Self { + family: family.into(), + given: None, + prefix: None, + suffix: None, + } + } + + /// Format for display: "Given prefix Family, Suffix" — natural reading order. + pub fn display_name(&self) -> String { + let mut parts = Vec::new(); + if let Some(given) = &self.given { + parts.push(given.as_str()); + } + if let Some(prefix) = &self.prefix { + parts.push(prefix.as_str()); + } + parts.push(self.family.as_str()); + let mut name = parts.join(" "); + if let Some(suffix) = &self.suffix { + name.push_str(", "); + name.push_str(suffix); + } + name + } + + /// Format as BibTeX expects: "{prefix} {family}, {suffix}, {given}". + /// Falls back gracefully when optional parts are absent. + pub fn to_bibtex(&self) -> String { + let mut family_part = String::new(); + if let Some(prefix) = &self.prefix { + family_part.push_str(prefix); + family_part.push(' '); + } + family_part.push_str(&self.family); + + match (&self.suffix, &self.given) { + (Some(suffix), Some(given)) => { + format!("{family_part}, {suffix}, {given}") + } + (Some(suffix), None) => format!("{family_part}, {suffix}"), + (None, Some(given)) => format!("{family_part}, {given}"), + (None, None) => family_part, + } + } +} + +impl fmt::Display for Person { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.display_name()) + } +} + +/// Standard BibTeX and common BibLaTeX entry types. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum EntryType { + Article, + Book, + Booklet, + InBook, + InCollection, + InProceedings, + Manual, + MastersThesis, + Misc, + PhdThesis, + Proceedings, + TechReport, + Unpublished, + Online, +} + +impl EntryType { + /// The BibTeX entry type name as it appears in `.bib` files. + pub fn bibtex_name(&self) -> &'static str { + match self { + EntryType::Article => "article", + EntryType::Book => "book", + EntryType::Booklet => "booklet", + EntryType::InBook => "inbook", + EntryType::InCollection => "incollection", + EntryType::InProceedings => "inproceedings", + EntryType::Manual => "manual", + EntryType::MastersThesis => "mastersthesis", + EntryType::Misc => "misc", + EntryType::PhdThesis => "phdthesis", + EntryType::Proceedings => "proceedings", + EntryType::TechReport => "techreport", + EntryType::Unpublished => "unpublished", + EntryType::Online => "online", + } + } +} + +/// A PDF file stored inside the Brittle repository. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PdfAttachment { + /// Path relative to the repository root (e.g., `"pdfs/550e8400-....pdf"`). + pub stored_path: PathBuf, + /// SHA-256 hex digest of the file contents for integrity verification. + pub content_hash: String, +} + +/// A citable work. The core entity of Brittle. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Reference { + pub id: ReferenceId, + /// The BibTeX cite key (e.g., `"knuth1984texbook"`). User-facing and mutable. + pub cite_key: String, + pub entry_type: EntryType, + /// Authors listed in order. + pub authors: Vec, + /// Editors (for edited books, proceedings, etc.). + pub editors: Vec, + /// All other fields (title, year, journal, volume, etc.) as plain strings. + /// BTreeMap for deterministic serialization order (important for git diffs). + pub fields: BTreeMap, + pub pdf: Option, + pub created_at: DateTime, + pub modified_at: DateTime, +} + +impl Reference { + pub fn new(cite_key: impl Into, entry_type: EntryType) -> Self { + let now = Utc::now(); + Self { + id: ReferenceId::new(), + cite_key: cite_key.into(), + entry_type, + authors: Vec::new(), + editors: Vec::new(), + fields: BTreeMap::new(), + pdf: None, + created_at: now, + modified_at: now, + } + } + + /// Returns the value of the `title` field, if present. + pub fn title(&self) -> Option<&str> { + self.fields.get("title").map(String::as_str) + } + + /// Returns the value of the `year` field, if present. + pub fn year(&self) -> Option<&str> { + self.fields.get("year").map(String::as_str) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn person_bibtex_full() { + let p = Person { + family: "Dijkstra".into(), + given: Some("Edsger W.".into()), + prefix: None, + suffix: None, + }; + assert_eq!(p.to_bibtex(), "Dijkstra, Edsger W."); + } + + #[test] + fn person_bibtex_with_prefix() { + let p = Person { + family: "Beethoven".into(), + given: Some("Ludwig".into()), + prefix: Some("van".into()), + suffix: None, + }; + assert_eq!(p.to_bibtex(), "van Beethoven, Ludwig"); + } + + #[test] + fn person_bibtex_with_suffix() { + let p = Person { + family: "King".into(), + given: Some("Martin Luther".into()), + prefix: None, + suffix: Some("Jr.".into()), + }; + assert_eq!(p.to_bibtex(), "King, Jr., Martin Luther"); + } + + #[test] + fn person_bibtex_family_only() { + let p = Person::new("Aristotle"); + assert_eq!(p.to_bibtex(), "Aristotle"); + } + + #[test] + fn reference_serde_round_trip() { + let mut r = Reference::new("doe2024", EntryType::Article); + r.authors.push(Person { + family: "Doe".into(), + given: Some("Jane".into()), + prefix: None, + suffix: None, + }); + r.fields.insert("title".into(), "A Great Paper".into()); + r.fields.insert("year".into(), "2024".into()); + + let toml_str = toml::to_string(&r).expect("serialize to TOML"); + let r2: Reference = toml::from_str(&toml_str).expect("deserialize from TOML"); + + assert_eq!(r.id, r2.id); + assert_eq!(r.cite_key, r2.cite_key); + assert_eq!(r.authors, r2.authors); + assert_eq!(r.fields, r2.fields); + } + + #[test] + fn entry_type_bibtex_names() { + assert_eq!(EntryType::Article.bibtex_name(), "article"); + assert_eq!(EntryType::InProceedings.bibtex_name(), "inproceedings"); + assert_eq!(EntryType::PhdThesis.bibtex_name(), "phdthesis"); + } +} diff --git a/brittle-core/src/model/snapshot.rs b/brittle-core/src/model/snapshot.rs new file mode 100644 index 0000000..c84bc33 --- /dev/null +++ b/brittle-core/src/model/snapshot.rs @@ -0,0 +1,12 @@ +use chrono::{DateTime, Utc}; +use serde::Serialize; + +/// Metadata about a stored snapshot (git commit). +/// Not serialized to files — read directly from git history. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct Snapshot { + /// Git commit SHA (hex string). + pub id: String, + pub message: String, + pub timestamp: DateTime, +} diff --git a/brittle-core/src/store/fs.rs b/brittle-core/src/store/fs.rs new file mode 100644 index 0000000..4555440 --- /dev/null +++ b/brittle-core/src/store/fs.rs @@ -0,0 +1,448 @@ +use crate::error::{EntityType, StoreError}; +use crate::model::{AnnotationSet, Library, LibraryId, Reference, ReferenceId, Snapshot}; +use crate::store::Store; +use chrono::{DateTime, TimeZone, Utc}; +use git2::{IndexAddOption, Repository, Signature}; +use std::path::{Path, PathBuf}; + +const REFERENCES_DIR: &str = "references"; +const LIBRARIES_DIR: &str = "libraries"; +const ANNOTATIONS_DIR: &str = "annotations"; +const PDFS_DIR: &str = "pdfs"; + +/// Filesystem + git-backed store. Each entity is a TOML file. +/// Snapshots are git commits; time travel is git checkout. +pub struct FsStore { + root: PathBuf, + repo: Repository, +} + +impl FsStore { + /// Create a new Brittle repository at the given path. + /// Fails if the path already contains a git repository. + pub fn create(path: &Path) -> Result { + if path.join(".git").exists() { + return Err(StoreError::RepoAlreadyExists { + path: path.to_owned(), + }); + } + + let repo = Repository::init(path).map_err(StoreError::Git)?; + + // Create subdirectories. + for dir in [REFERENCES_DIR, LIBRARIES_DIR, ANNOTATIONS_DIR, PDFS_DIR] { + std::fs::create_dir_all(path.join(dir))?; + } + + let mut store = Self { + root: path.to_owned(), + repo, + }; + + // Create the initial commit so the repo has a HEAD. + store.commit_all("Initialize Brittle repository")?; + + Ok(store) + } + + /// Open an existing Brittle repository. + pub fn open(path: &Path) -> Result { + let repo = Repository::open(path).map_err(|_| StoreError::RepoNotFound { + path: path.to_owned(), + })?; + Ok(Self { + root: path.to_owned(), + repo, + }) + } + + /// Stage all changes and create a git commit. Returns the commit OID as hex. + fn commit_all(&mut self, message: &str) -> Result { + let mut index = self.repo.index().map_err(StoreError::Git)?; + index + .add_all(["*"].iter(), IndexAddOption::DEFAULT, None) + .map_err(StoreError::Git)?; + index.write().map_err(StoreError::Git)?; + + let tree_oid = index.write_tree().map_err(StoreError::Git)?; + let tree = self.repo.find_tree(tree_oid).map_err(StoreError::Git)?; + + let sig = Signature::now("Brittle", "brittle@local").map_err(StoreError::Git)?; + + let parent_commit = self.repo.head().ok().and_then(|h| h.peel_to_commit().ok()); + + let oid = match &parent_commit { + Some(parent) => self + .repo + .commit(Some("HEAD"), &sig, &sig, message, &tree, &[parent]) + .map_err(StoreError::Git)?, + None => self + .repo + .commit(Some("HEAD"), &sig, &sig, message, &tree, &[]) + .map_err(StoreError::Git)?, + }; + + Ok(oid.to_string()) + } + + fn reference_path(&self, id: ReferenceId) -> PathBuf { + self.root.join(REFERENCES_DIR).join(format!("{id}.toml")) + } + + fn library_path(&self, id: LibraryId) -> PathBuf { + self.root.join(LIBRARIES_DIR).join(format!("{id}.toml")) + } + + fn annotation_path(&self, ref_id: ReferenceId) -> PathBuf { + self.root + .join(ANNOTATIONS_DIR) + .join(format!("{ref_id}.toml")) + } + + pub fn pdf_dir(&self) -> PathBuf { + self.root.join(PDFS_DIR) + } + + /// Returns the repository root directory. + pub fn root(&self) -> &Path { + &self.root + } + + fn write_toml(&self, path: &Path, value: &T) -> Result<(), StoreError> { + let content = toml::to_string(value).map_err(|e| StoreError::Serialization { + message: e.to_string(), + })?; + std::fs::write(path, content)?; + Ok(()) + } + + fn read_toml(&self, path: &Path) -> Result { + let content = std::fs::read_to_string(path)?; + toml::from_str(&content).map_err(|e| StoreError::Deserialization { + path: path.to_owned(), + message: e.to_string(), + }) + } + + fn ids_from_dir(&self, dir: &str, parse: F) -> Result, StoreError> + where + F: Fn(&str) -> Option, + { + let dir_path = self.root.join(dir); + let mut ids = Vec::new(); + for entry in std::fs::read_dir(&dir_path)? { + let entry = entry?; + let name = entry.file_name(); + let name = name.to_string_lossy(); + if let Some(stem) = name.strip_suffix(".toml") + && let Some(id) = parse(stem) + { + ids.push(id); + } + } + Ok(ids) + } +} + +impl Store for FsStore { + fn save_reference(&mut self, reference: &Reference) -> Result<(), StoreError> { + self.write_toml(&self.reference_path(reference.id), reference) + } + + fn load_reference(&self, id: ReferenceId) -> Result { + let path = self.reference_path(id); + if !path.exists() { + return Err(StoreError::NotFound { + entity_type: EntityType::Reference, + id: id.to_string(), + }); + } + self.read_toml(&path) + } + + fn delete_reference(&mut self, id: ReferenceId) -> Result<(), StoreError> { + let path = self.reference_path(id); + if !path.exists() { + return Err(StoreError::NotFound { + entity_type: EntityType::Reference, + id: id.to_string(), + }); + } + std::fs::remove_file(path)?; + Ok(()) + } + + fn list_reference_ids(&self) -> Result, StoreError> { + self.ids_from_dir(REFERENCES_DIR, |s| { + s.parse::().ok().map(ReferenceId::from) + }) + } + + fn save_library(&mut self, library: &Library) -> Result<(), StoreError> { + self.write_toml(&self.library_path(library.id), library) + } + + fn load_library(&self, id: LibraryId) -> Result { + let path = self.library_path(id); + if !path.exists() { + return Err(StoreError::NotFound { + entity_type: EntityType::Library, + id: id.to_string(), + }); + } + self.read_toml(&path) + } + + fn delete_library(&mut self, id: LibraryId) -> Result<(), StoreError> { + let path = self.library_path(id); + if !path.exists() { + return Err(StoreError::NotFound { + entity_type: EntityType::Library, + id: id.to_string(), + }); + } + std::fs::remove_file(path)?; + Ok(()) + } + + fn list_library_ids(&self) -> Result, StoreError> { + self.ids_from_dir(LIBRARIES_DIR, |s| { + s.parse::().ok().map(LibraryId::from) + }) + } + + fn load_annotations(&self, ref_id: ReferenceId) -> Result { + let path = self.annotation_path(ref_id); + if !path.exists() { + return Ok(AnnotationSet::new(ref_id)); + } + self.read_toml(&path) + } + + fn save_annotations(&mut self, set: &AnnotationSet) -> Result<(), StoreError> { + self.write_toml(&self.annotation_path(set.reference_id), set) + } + + fn delete_annotations(&mut self, ref_id: ReferenceId) -> Result<(), StoreError> { + let path = self.annotation_path(ref_id); + if path.exists() { + std::fs::remove_file(path)?; + } + Ok(()) + } + + fn create_snapshot(&mut self, message: &str) -> Result { + let oid = self.commit_all(message)?; + let commit = self + .repo + .find_commit(git2::Oid::from_str(&oid).map_err(StoreError::Git)?) + .map_err(StoreError::Git)?; + let timestamp = commit_timestamp(&commit)?; + + Ok(Snapshot { + id: oid, + message: message.to_owned(), + timestamp, + }) + } + + fn list_snapshots(&self) -> Result, StoreError> { + let mut revwalk = self.repo.revwalk().map_err(StoreError::Git)?; + revwalk.push_head().map_err(StoreError::Git)?; + revwalk + .set_sorting(git2::Sort::TIME) + .map_err(StoreError::Git)?; + + let mut snapshots = Vec::new(); + for oid in revwalk { + let oid = oid.map_err(StoreError::Git)?; + let commit = self.repo.find_commit(oid).map_err(StoreError::Git)?; + let message = commit.message().unwrap_or("").to_owned(); + let timestamp = commit_timestamp(&commit)?; + snapshots.push(Snapshot { + id: oid.to_string(), + message, + timestamp, + }); + } + + Ok(snapshots) + } + + fn restore_snapshot(&mut self, snapshot_id: &str) -> Result<(), StoreError> { + let oid = git2::Oid::from_str(snapshot_id).map_err(|_| StoreError::NotFound { + entity_type: EntityType::Snapshot, + id: snapshot_id.to_owned(), + })?; + + let commit = self + .repo + .find_commit(oid) + .map_err(|_| StoreError::NotFound { + entity_type: EntityType::Snapshot, + id: snapshot_id.to_owned(), + })?; + + let tree = commit.tree().map_err(StoreError::Git)?; + + // Checkout the tree, updating both the index and the working directory. + // `force` overwrites modified tracked files; `remove_untracked` removes + // files that were written since the last snapshot but never committed. + let mut checkout_opts = git2::build::CheckoutBuilder::new(); + checkout_opts.force().remove_untracked(true); + self.repo + .checkout_tree(tree.as_object(), Some(&mut checkout_opts)) + .map_err(StoreError::Git)?; + + // Move HEAD to point at the restored commit. + self.repo.set_head_detached(oid).map_err(StoreError::Git)?; + + Ok(()) + } + + fn has_uncommitted_changes(&self) -> Result { + let statuses = self + .repo + .statuses(Some( + git2::StatusOptions::new() + .include_untracked(true) + .recurse_untracked_dirs(true), + )) + .map_err(StoreError::Git)?; + Ok(!statuses.is_empty()) + } +} + +fn commit_timestamp(commit: &git2::Commit<'_>) -> Result, StoreError> { + let time = commit.time(); + Utc.timestamp_opt(time.seconds(), 0) + .single() + .ok_or_else(|| StoreError::Serialization { + message: "invalid commit timestamp".into(), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::model::{EntryType, Library, Reference}; + + fn make_store(dir: &Path) -> FsStore { + FsStore::create(dir).expect("create store") + } + + #[test] + fn create_and_open() { + let tmp = tempfile::tempdir().unwrap(); + let store = make_store(tmp.path()); + drop(store); + FsStore::open(tmp.path()).expect("re-open store"); + } + + #[test] + fn create_fails_if_repo_exists() { + let tmp = tempfile::tempdir().unwrap(); + make_store(tmp.path()); + assert!(FsStore::create(tmp.path()).is_err()); + } + + #[test] + fn save_load_delete_reference() { + let tmp = tempfile::tempdir().unwrap(); + let mut store = make_store(tmp.path()); + let r = Reference::new("test2024", EntryType::Article); + let id = r.id; + + store.save_reference(&r).unwrap(); + let loaded = store.load_reference(id).unwrap(); + assert_eq!(loaded.cite_key, "test2024"); + + store.delete_reference(id).unwrap(); + assert!(store.load_reference(id).is_err()); + } + + #[test] + fn list_reference_ids() { + let tmp = tempfile::tempdir().unwrap(); + let mut store = make_store(tmp.path()); + let r1 = Reference::new("a2024", EntryType::Article); + let r2 = Reference::new("b2024", EntryType::Book); + store.save_reference(&r1).unwrap(); + store.save_reference(&r2).unwrap(); + + let ids = store.list_reference_ids().unwrap(); + assert_eq!(ids.len(), 2); + } + + #[test] + fn save_load_library() { + let tmp = tempfile::tempdir().unwrap(); + let mut store = make_store(tmp.path()); + let lib = Library::new("ML Papers", None); + let id = lib.id; + + store.save_library(&lib).unwrap(); + let loaded = store.load_library(id).unwrap(); + assert_eq!(loaded.name, "ML Papers"); + } + + #[test] + fn annotations_missing_returns_empty_set() { + let tmp = tempfile::tempdir().unwrap(); + let store = make_store(tmp.path()); + let ref_id = ReferenceId::new(); + let set = store.load_annotations(ref_id).unwrap(); + assert!(set.annotations.is_empty()); + } + + #[test] + fn create_and_list_snapshot() { + let tmp = tempfile::tempdir().unwrap(); + let mut store = make_store(tmp.path()); + + // Save something so there's content to commit beyond the initial commit. + let r = Reference::new("snap2024", EntryType::Misc); + store.save_reference(&r).unwrap(); + let snap = store.create_snapshot("my first snapshot").unwrap(); + + let snapshots = store.list_snapshots().unwrap(); + assert!(snapshots.iter().any(|s| s.id == snap.id)); + assert!(snapshots.iter().any(|s| s.message == "my first snapshot")); + } + + #[test] + fn restore_snapshot_reverts_state() { + let tmp = tempfile::tempdir().unwrap(); + let mut store = make_store(tmp.path()); + + let r = Reference::new("before2024", EntryType::Article); + let ref_id = r.id; + store.save_reference(&r).unwrap(); + let snap = store.create_snapshot("baseline").unwrap(); + + // Modify state: add another reference. + let r2 = Reference::new("after2024", EntryType::Book); + store.save_reference(&r2).unwrap(); + assert_eq!(store.list_reference_ids().unwrap().len(), 2); + + // Restore to baseline — should have only 1 reference. + store.restore_snapshot(&snap.id).unwrap(); + + let store2 = FsStore::open(tmp.path()).unwrap(); + assert_eq!(store2.list_reference_ids().unwrap().len(), 1); + assert!(store2.load_reference(ref_id).is_ok()); + } + + #[test] + fn has_uncommitted_changes_detects_new_files() { + let tmp = tempfile::tempdir().unwrap(); + let mut store = make_store(tmp.path()); + + assert!(!store.has_uncommitted_changes().unwrap()); + + let r = Reference::new("new2024", EntryType::Misc); + store.save_reference(&r).unwrap(); + + assert!(store.has_uncommitted_changes().unwrap()); + } +} diff --git a/brittle-core/src/store/memory.rs b/brittle-core/src/store/memory.rs new file mode 100644 index 0000000..1289ace --- /dev/null +++ b/brittle-core/src/store/memory.rs @@ -0,0 +1,302 @@ +use crate::error::{EntityType, StoreError}; +use crate::model::{AnnotationSet, Library, LibraryId, Reference, ReferenceId, Snapshot}; +use crate::store::Store; +use chrono::Utc; +use std::collections::HashMap; + +/// In-memory store for testing. Not suitable for production use. +#[derive(Debug, Default)] +pub struct MemoryStore { + references: HashMap, + libraries: HashMap, + annotations: HashMap, + /// Checkpoints for snapshot simulation: (id, message, cloned state). + snapshots: Vec<(String, String, Box)>, + next_snapshot_idx: usize, +} + +#[derive(Debug)] +struct MemorySnapshot { + references: HashMap, + libraries: HashMap, + annotations: HashMap, +} + +impl MemoryStore { + pub fn new() -> Self { + Self::default() + } +} + +impl Store for MemoryStore { + fn save_reference(&mut self, reference: &Reference) -> Result<(), StoreError> { + self.references.insert(reference.id, reference.clone()); + Ok(()) + } + + fn load_reference(&self, id: ReferenceId) -> Result { + self.references + .get(&id) + .cloned() + .ok_or_else(|| StoreError::NotFound { + entity_type: EntityType::Reference, + id: id.to_string(), + }) + } + + fn delete_reference(&mut self, id: ReferenceId) -> Result<(), StoreError> { + self.references + .remove(&id) + .ok_or_else(|| StoreError::NotFound { + entity_type: EntityType::Reference, + id: id.to_string(), + })?; + Ok(()) + } + + fn list_reference_ids(&self) -> Result, StoreError> { + Ok(self.references.keys().copied().collect()) + } + + fn save_library(&mut self, library: &Library) -> Result<(), StoreError> { + self.libraries.insert(library.id, library.clone()); + Ok(()) + } + + fn load_library(&self, id: LibraryId) -> Result { + self.libraries + .get(&id) + .cloned() + .ok_or_else(|| StoreError::NotFound { + entity_type: EntityType::Library, + id: id.to_string(), + }) + } + + fn delete_library(&mut self, id: LibraryId) -> Result<(), StoreError> { + self.libraries + .remove(&id) + .ok_or_else(|| StoreError::NotFound { + entity_type: EntityType::Library, + id: id.to_string(), + })?; + Ok(()) + } + + fn list_library_ids(&self) -> Result, StoreError> { + Ok(self.libraries.keys().copied().collect()) + } + + fn load_annotations(&self, ref_id: ReferenceId) -> Result { + Ok(self + .annotations + .get(&ref_id) + .cloned() + .unwrap_or_else(|| AnnotationSet::new(ref_id))) + } + + fn save_annotations(&mut self, set: &AnnotationSet) -> Result<(), StoreError> { + self.annotations.insert(set.reference_id, set.clone()); + Ok(()) + } + + fn delete_annotations(&mut self, ref_id: ReferenceId) -> Result<(), StoreError> { + self.annotations.remove(&ref_id); + Ok(()) + } + + fn create_snapshot(&mut self, message: &str) -> Result { + let id = format!("mem-snapshot-{:04}", self.next_snapshot_idx); + self.next_snapshot_idx += 1; + let snapshot_data = Box::new(MemorySnapshot { + references: self.references.clone(), + libraries: self.libraries.clone(), + annotations: self.annotations.clone(), + }); + let timestamp = Utc::now(); + self.snapshots + .push((id.clone(), message.to_owned(), snapshot_data)); + Ok(Snapshot { + id, + message: message.to_owned(), + timestamp, + }) + } + + fn list_snapshots(&self) -> Result, StoreError> { + let snapshots = self + .snapshots + .iter() + .rev() + .map(|(id, message, _)| Snapshot { + id: id.clone(), + message: message.clone(), + timestamp: Utc::now(), // timestamps not stored in MemoryStore + }) + .collect(); + Ok(snapshots) + } + + fn restore_snapshot(&mut self, snapshot_id: &str) -> Result<(), StoreError> { + let snapshot = self + .snapshots + .iter() + .find(|(id, _, _)| id == snapshot_id) + .ok_or_else(|| StoreError::NotFound { + entity_type: EntityType::Snapshot, + id: snapshot_id.to_owned(), + })?; + self.references = snapshot.2.references.clone(); + self.libraries = snapshot.2.libraries.clone(); + self.annotations = snapshot.2.annotations.clone(); + Ok(()) + } + + fn has_uncommitted_changes(&self) -> Result { + // MemoryStore has no concept of uncommitted changes. + Ok(false) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::model::{AnnotationType, EntryType, Library, Reference, TextMarkupType}; + + fn make_reference() -> Reference { + Reference::new("test2024", EntryType::Article) + } + + fn make_library() -> Library { + Library::new("Test Library", None) + } + + #[test] + fn save_and_load_reference() { + let mut store = MemoryStore::new(); + let r = make_reference(); + let id = r.id; + store.save_reference(&r).unwrap(); + let r2 = store.load_reference(id).unwrap(); + assert_eq!(r.cite_key, r2.cite_key); + } + + #[test] + fn load_missing_reference_returns_error() { + let store = MemoryStore::new(); + let id = ReferenceId::new(); + let err = store.load_reference(id).unwrap_err(); + assert!(matches!( + err, + StoreError::NotFound { + entity_type: EntityType::Reference, + .. + } + )); + } + + #[test] + fn delete_reference() { + let mut store = MemoryStore::new(); + let r = make_reference(); + let id = r.id; + store.save_reference(&r).unwrap(); + store.delete_reference(id).unwrap(); + assert!(store.load_reference(id).is_err()); + } + + #[test] + fn list_reference_ids() { + let mut store = MemoryStore::new(); + let r1 = make_reference(); + let r2 = make_reference(); + store.save_reference(&r1).unwrap(); + store.save_reference(&r2).unwrap(); + let ids = store.list_reference_ids().unwrap(); + assert_eq!(ids.len(), 2); + assert!(ids.contains(&r1.id)); + assert!(ids.contains(&r2.id)); + } + + #[test] + fn save_and_load_library() { + let mut store = MemoryStore::new(); + let lib = make_library(); + let id = lib.id; + store.save_library(&lib).unwrap(); + let lib2 = store.load_library(id).unwrap(); + assert_eq!(lib.name, lib2.name); + } + + #[test] + fn delete_library() { + let mut store = MemoryStore::new(); + let lib = make_library(); + let id = lib.id; + store.save_library(&lib).unwrap(); + store.delete_library(id).unwrap(); + assert!(store.load_library(id).is_err()); + } + + #[test] + fn annotations_default_to_empty_set() { + let store = MemoryStore::new(); + let ref_id = ReferenceId::new(); + let set = store.load_annotations(ref_id).unwrap(); + assert_eq!(set.reference_id, ref_id); + assert!(set.annotations.is_empty()); + } + + #[test] + fn save_and_load_annotations() { + use crate::model::{Annotation, Color}; + + let mut store = MemoryStore::new(); + let ref_id = ReferenceId::new(); + let ann = Annotation::new( + ref_id, + 0, + AnnotationType::TextMarkup { + markup_type: TextMarkupType::Highlight, + quads: vec![], + color: Color::YELLOW, + selected_text: None, + }, + ); + let set = AnnotationSet { + reference_id: ref_id, + annotations: vec![ann], + }; + store.save_annotations(&set).unwrap(); + let set2 = store.load_annotations(ref_id).unwrap(); + assert_eq!(set2.annotations.len(), 1); + } + + #[test] + fn snapshot_create_and_restore() { + let mut store = MemoryStore::new(); + let r = make_reference(); + let ref_id = r.id; + store.save_reference(&r).unwrap(); + + let snap = store.create_snapshot("first snapshot").unwrap(); + + // Modify state after snapshot. + store.delete_reference(ref_id).unwrap(); + assert!(store.load_reference(ref_id).is_err()); + + // Restore snapshot. + store.restore_snapshot(&snap.id).unwrap(); + assert!(store.load_reference(ref_id).is_ok()); + } + + #[test] + fn list_snapshots_in_reverse_order() { + let mut store = MemoryStore::new(); + store.create_snapshot("first").unwrap(); + store.create_snapshot("second").unwrap(); + let snaps = store.list_snapshots().unwrap(); + assert_eq!(snaps.len(), 2); + assert_eq!(snaps[0].message, "second"); // most recent first + } +} diff --git a/brittle-core/src/store/mod.rs b/brittle-core/src/store/mod.rs new file mode 100644 index 0000000..4d5d547 --- /dev/null +++ b/brittle-core/src/store/mod.rs @@ -0,0 +1,44 @@ +pub mod fs; +pub mod memory; + +use crate::error::StoreError; +use crate::model::{AnnotationSet, Library, LibraryId, Reference, ReferenceId, Snapshot}; + +/// Abstraction over the storage backend. +/// +/// The git-backed filesystem (`FsStore`) is the production implementation. +/// An in-memory implementation (`MemoryStore`) exists for testing. +pub trait Store { + // ---- References ---- + + fn save_reference(&mut self, reference: &Reference) -> Result<(), StoreError>; + fn load_reference(&self, id: ReferenceId) -> Result; + fn delete_reference(&mut self, id: ReferenceId) -> Result<(), StoreError>; + fn list_reference_ids(&self) -> Result, StoreError>; + + // ---- Libraries ---- + + fn save_library(&mut self, library: &Library) -> Result<(), StoreError>; + fn load_library(&self, id: LibraryId) -> Result; + fn delete_library(&mut self, id: LibraryId) -> Result<(), StoreError>; + fn list_library_ids(&self) -> Result, StoreError>; + + // ---- Annotations ---- + + /// Load the annotation set for a reference. Returns an empty set if none exists. + fn load_annotations(&self, ref_id: ReferenceId) -> Result; + fn save_annotations(&mut self, set: &AnnotationSet) -> Result<(), StoreError>; + fn delete_annotations(&mut self, ref_id: ReferenceId) -> Result<(), StoreError>; + + // ---- Snapshots ---- + + fn create_snapshot(&mut self, message: &str) -> Result; + fn list_snapshots(&self) -> Result, StoreError>; + /// Restore to a previous snapshot. Caller must ensure no uncommitted changes exist. + fn restore_snapshot(&mut self, snapshot_id: &str) -> Result<(), StoreError>; + fn has_uncommitted_changes(&self) -> Result; +} + +// Re-export concrete types for convenience. +pub use fs::FsStore; +pub use memory::MemoryStore; diff --git a/brittle-core/tests/end_to_end.rs b/brittle-core/tests/end_to_end.rs new file mode 100644 index 0000000..cd9099d --- /dev/null +++ b/brittle-core/tests/end_to_end.rs @@ -0,0 +1,163 @@ +/// End-to-end integration test using a real Brittle repository. +/// +/// Exercises the full workflow: create repo, add references with authors, +/// organize in libraries, export BibTeX, create a snapshot, modify state, +/// restore the snapshot, and verify everything reverted correctly. +use brittle_core::{ + AnnotationType, Brittle, BrittleError, Color, EntryType, Person, TextMarkupType, + ValidationError, +}; + +#[test] +fn full_workflow() { + let tmp = tempfile::tempdir().unwrap(); + let mut db = Brittle::create(tmp.path()).unwrap(); + + // ---- Create references ---- + let mut turing = db + .create_reference("turing1950", EntryType::Article) + .unwrap(); + turing.authors.push(Person { + family: "Turing".into(), + given: Some("Alan M.".into()), + prefix: None, + suffix: None, + }); + turing.fields.insert( + "title".into(), + "Computing Machinery and Intelligence".into(), + ); + turing.fields.insert("journal".into(), "Mind".into()); + turing.fields.insert("year".into(), "1950".into()); + let turing = db.update_reference(turing).unwrap(); + + let mut knuth = db.create_reference("knuth1984", EntryType::Book).unwrap(); + knuth.authors.push(Person::new("Knuth")); + knuth.fields.insert("title".into(), "The TeXbook".into()); + knuth + .fields + .insert("publisher".into(), "Addison-Wesley".into()); + knuth.fields.insert("year".into(), "1984".into()); + let knuth = db.update_reference(knuth).unwrap(); + + // ---- Organize in libraries ---- + let cs = db.create_library("Computer Science", None).unwrap(); + let ai = db.create_library("AI", Some(cs.id)).unwrap(); + + db.add_to_library(cs.id, turing.id).unwrap(); + db.add_to_library(ai.id, turing.id).unwrap(); // multi-membership + db.add_to_library(cs.id, knuth.id).unwrap(); + + // Both references are in CS. + let cs_refs = db.list_library_references(cs.id).unwrap(); + assert_eq!(cs_refs.len(), 2); + + // Turing is in both CS and AI. + let turing_libs = db.list_reference_libraries(turing.id).unwrap(); + assert_eq!(turing_libs.len(), 2); + + // ---- BibTeX export ---- + let (bibtex, errors) = db.export_library_bibtex(cs.id).unwrap(); + assert!(errors.is_empty(), "unexpected BibTeX errors: {errors:?}"); + assert!(bibtex.contains("@article{turing1950,")); + assert!(bibtex.contains("Turing, Alan M.")); + assert!(bibtex.contains("Computing Machinery and Intelligence")); + assert!(bibtex.contains("@book{knuth1984,")); + + // ---- Annotations ---- + let ann = db + .create_annotation( + turing.id, + 0, + AnnotationType::TextMarkup { + markup_type: TextMarkupType::Highlight, + quads: vec![], + color: Color::YELLOW, + selected_text: Some("The Imitation Game".into()), + }, + Some("Key concept".into()), + ) + .unwrap(); + + let annotations = db.get_annotations(turing.id).unwrap(); + assert_eq!(annotations.len(), 1); + assert_eq!(annotations[0].content.as_deref(), Some("Key concept")); + + // ---- Snapshot ---- + let snap = db + .create_snapshot("Baseline with Turing and Knuth") + .unwrap(); + assert!(!snap.id.is_empty()); + + let snapshots = db.list_snapshots().unwrap(); + // At least our named snapshot + the initial "Initialize Brittle repository" commit. + assert!(snapshots.len() >= 2); + assert!( + snapshots + .iter() + .any(|s| s.message == "Baseline with Turing and Knuth") + ); + + // ---- Modify state after snapshot ---- + db.delete_reference(knuth.id).unwrap(); + assert!(db.get_reference(knuth.id).is_err()); + + let cs_refs_after = db.list_library_references(cs.id).unwrap(); + assert_eq!( + cs_refs_after.len(), + 1, + "Knuth should have been removed from library" + ); + + // ---- Restore snapshot ---- + // The knuth deletion is written to disk but not committed — verify this. + assert!(db.has_uncommitted_changes().unwrap()); + + // restore_snapshot errors on uncommitted changes; use discard_changes instead. + db.discard_changes().unwrap(); + + // After restore, Knuth should be back. + let knuth_restored = db.get_reference(knuth.id).unwrap(); + assert_eq!(knuth_restored.cite_key, "knuth1984"); + + // CS library should have 2 members again. + let cs_refs_restored = db.list_library_references(cs.id).unwrap(); + assert_eq!(cs_refs_restored.len(), 2); + + // Inspect git log to verify history is human-readable. + let snapshots_after = db.list_snapshots().unwrap(); + assert!(!snapshots_after.is_empty()); +} + +#[test] +fn get_pdf_path_returns_error_when_no_pdf_attached() { + let tmp = tempfile::tempdir().unwrap(); + let mut db = Brittle::create(tmp.path()).unwrap(); + let r = db.create_reference("nopdf2024", EntryType::Misc).unwrap(); + + let err = db.get_pdf_path(r.id).unwrap_err(); + assert!( + matches!( + err, + BrittleError::Validation(ValidationError::NoPdfAttached { .. }) + ), + "expected NoPdfAttached, got {err}" + ); +} + +#[test] +fn get_pdf_path_returns_path_after_attach() { + let tmp = tempfile::tempdir().unwrap(); + let mut db = Brittle::create(tmp.path()).unwrap(); + let r = db.create_reference("withpdf2024", EntryType::Misc).unwrap(); + + // Write a dummy PDF file. + let source = tmp.path().join("dummy.pdf"); + std::fs::write(&source, b"%PDF-1.4 dummy").unwrap(); + + db.attach_pdf(r.id, &source).unwrap(); + + let path = db.get_pdf_path(r.id).unwrap(); + assert!(path.exists(), "PDF path {path:?} should exist on disk"); + assert_eq!(path.extension().and_then(|e| e.to_str()), Some("pdf")); +} diff --git a/brittle-keymap/Cargo.toml b/brittle-keymap/Cargo.toml new file mode 100644 index 0000000..7b095bb --- /dev/null +++ b/brittle-keymap/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "brittle-keymap" +version = "0.1.0" +edition = "2021" diff --git a/brittle-keymap/src/actions.rs b/brittle-keymap/src/actions.rs new file mode 100644 index 0000000..9b89f5e --- /dev/null +++ b/brittle-keymap/src/actions.rs @@ -0,0 +1,72 @@ +//! Canonical action name constants used by both the default bindings and the UI. +//! +//! Every string that the keymap can emit as an action name is defined here. +//! The UI dispatches on these to decide what to do. + +// ── Navigation (within the focused pane) ───────────────────────────────────── + +/// Move the selection cursor one step down (repeated `count` times). +pub const NAV_DOWN: &str = "nav.down"; +/// Move the selection cursor one step up (repeated `count` times). +pub const NAV_UP: &str = "nav.up"; +/// Jump to the first item. +pub const NAV_TOP: &str = "nav.top"; +/// Jump to the last item. +pub const NAV_BOTTOM: &str = "nav.bottom"; +/// Scroll / page down. +pub const NAV_PAGE_DOWN: &str = "nav.page.down"; +/// Scroll / page up. +pub const NAV_PAGE_UP: &str = "nav.page.up"; + +// ── Pane focus ──────────────────────────────────────────────────────────────── + +/// Focus the left pane (library tree). +pub const FOCUS_LEFT: &str = "focus.left"; +/// Focus the centre pane (reference list). +pub const FOCUS_CENTER: &str = "focus.center"; +/// Focus the right pane (reference detail / editor). +pub const FOCUS_RIGHT: &str = "focus.right"; +/// Move focus to the next pane (cycles left → centre → right → left). +pub const FOCUS_NEXT: &str = "focus.next"; +/// Move focus to the previous pane. +pub const FOCUS_PREV: &str = "focus.prev"; + +// ── Library tree ────────────────────────────────────────────────────────────── + +/// Expand the selected tree node. +pub const TREE_EXPAND: &str = "tree.expand"; +/// Collapse the selected tree node. +pub const TREE_COLLAPSE: &str = "tree.collapse"; +/// Toggle the selected tree node open/closed. +pub const TREE_TOGGLE: &str = "tree.toggle"; + +// ── Item actions ────────────────────────────────────────────────────────────── + +/// Open the selected item (load PDF, expand library, etc.). +pub const ACTION_OPEN: &str = "action.open"; +/// Begin editing the selected item. +pub const ACTION_EDIT: &str = "action.edit"; +/// Delete the selected item. +pub const ACTION_DELETE: &str = "action.delete"; +/// Create a new item in the current context. +pub const ACTION_NEW: &str = "action.new"; + +// ── Tabs ────────────────────────────────────────────────────────────────────── + +/// Cycle to the next tab (wraps around). +pub const TAB_NEXT: &str = "tab.next"; +/// Cycle to the previous tab (wraps around). +pub const TAB_PREV: &str = "tab.prev"; +/// Close the current tab (no-op on the Library tab). +pub const TAB_CLOSE: &str = "tab.close"; + +// ── Input modes ─────────────────────────────────────────────────────────────── + +/// Enter command mode (the `:` prompt). +pub const MODE_COMMAND: &str = "mode.command"; +/// Enter search / filter mode (the `/` prompt). +pub const MODE_SEARCH: &str = "mode.search"; +/// Return to normal mode (dismiss any prompt, clear pending sequence). +pub const MODE_NORMAL: &str = "mode.normal"; +/// Export current view as BibTeX. +pub const MODE_BIBTEX: &str = "mode.bibtex"; diff --git a/brittle-keymap/src/binding.rs b/brittle-keymap/src/binding.rs new file mode 100644 index 0000000..f277175 --- /dev/null +++ b/brittle-keymap/src/binding.rs @@ -0,0 +1,198 @@ +//! Binding set: maps key sequences to action names. + +use crate::key::{Key, ParseError}; + +/// A named binding: a key sequence that triggers an action. +#[derive(Clone, Debug)] +pub struct Binding { + /// The ordered sequence of keys that must be pressed. + pub keys: Vec, + /// The action name emitted when the sequence completes. + pub action: String, +} + +/// Result of looking up a (partial) key sequence in a [`BindingSet`]. +#[derive(Debug, PartialEq, Eq)] +pub enum LookupResult { + /// The sequence is an exact match for a binding. + Exact(String), + /// The sequence is a valid prefix of one or more bindings; keep waiting. + Prefix, + /// The sequence matches nothing. + NoMatch, +} + +/// A collection of key→action bindings. +/// +/// Internally stored as a flat `Vec`; acceptable because the number of +/// bindings is small (typically < 100) and sequences are short (≤ 4 keys). +#[derive(Default, Clone, Debug)] +pub struct BindingSet { + bindings: Vec, +} + +impl BindingSet { + pub fn new() -> Self { + Self::default() + } + + /// Add a binding from pre-parsed keys. + pub fn add(&mut self, keys: Vec, action: impl Into) { + self.bindings.push(Binding { + keys, + action: action.into(), + }); + } + + /// Add a binding by parsing the key sequence string. + /// + /// Returns the `ParseError` if the string is not valid. + pub fn add_parsed( + &mut self, + key_sequence: &str, + action: impl Into, + ) -> Result<(), ParseError> { + let keys = crate::key::parse_sequence(key_sequence)?; + self.add(keys, action); + Ok(()) + } + + /// Look up a (possibly partial) key sequence. + /// + /// If a sequence is both an exact match *and* a prefix of longer bindings, + /// the exact match takes priority (no ambiguity). + pub fn lookup(&self, keys: &[Key]) -> LookupResult { + let mut found_prefix = false; + + for binding in &self.bindings { + if binding.keys == keys { + return LookupResult::Exact(binding.action.clone()); + } + if binding.keys.starts_with(keys) && binding.keys.len() > keys.len() { + found_prefix = true; + } + } + + if found_prefix { + LookupResult::Prefix + } else { + LookupResult::NoMatch + } + } + + /// Apply user-defined overrides on top of this binding set. + /// + /// For each `(action, key_sequence)` pair in `overrides`: + /// - All existing bindings for that action are removed. + /// - A new binding from the parsed sequence is added. + /// + /// Unknown action names are added as new bindings; parse errors are skipped. + pub fn apply_overrides<'a>(&mut self, overrides: impl IntoIterator) { + for (action, key_seq) in overrides { + // Remove existing bindings for this action. + self.bindings.retain(|b| b.action != action); + // Add the new binding (skip on parse error). + if let Ok(keys) = crate::key::parse_sequence(key_seq) { + self.add(keys, action); + } + } + } + + /// Return all bindings for inspection. + pub fn bindings(&self) -> &[Binding] { + &self.bindings + } +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use crate::key::Key; + + fn set_with_defaults() -> BindingSet { + let mut s = BindingSet::new(); + s.add_parsed("j", "nav.down").unwrap(); + s.add_parsed("k", "nav.up").unwrap(); + s.add_parsed("", "nav.down").unwrap(); + s.add_parsed("gg", "nav.top").unwrap(); + s.add_parsed("G", "nav.bottom").unwrap(); + s.add_parsed("zo", "tree.expand").unwrap(); + s + } + + #[test] + fn exact_single_key() { + let s = set_with_defaults(); + let keys = vec![Key::char('j')]; + assert_eq!(s.lookup(&keys), LookupResult::Exact("nav.down".into())); + } + + #[test] + fn exact_multi_key() { + let s = set_with_defaults(); + let keys = vec![Key::char('g'), Key::char('g')]; + assert_eq!(s.lookup(&keys), LookupResult::Exact("nav.top".into())); + } + + #[test] + fn prefix_of_multi_key_binding() { + let s = set_with_defaults(); + let keys = vec![Key::char('g')]; + assert_eq!(s.lookup(&keys), LookupResult::Prefix); + } + + #[test] + fn no_match() { + let s = set_with_defaults(); + let keys = vec![Key::char('x')]; + assert_eq!(s.lookup(&keys), LookupResult::NoMatch); + } + + #[test] + fn no_match_wrong_continuation() { + let s = set_with_defaults(); + // "gx" should not match anything. + let keys = vec![Key::char('g'), Key::char('x')]; + assert_eq!(s.lookup(&keys), LookupResult::NoMatch); + } + + #[test] + fn apply_overrides_replaces_action_binding() { + let mut s = set_with_defaults(); + // Replace nav.down from "j" to "n". + s.apply_overrides([("nav.down", "n")]); + + // Old "j" binding is gone. + assert_eq!(s.lookup(&[Key::char('j')]), LookupResult::NoMatch); + // New "n" binding is present. + assert_eq!( + s.lookup(&[Key::char('n')]), + LookupResult::Exact("nav.down".into()) + ); + } + + #[test] + fn apply_overrides_removes_all_bindings_for_action() { + let mut s = set_with_defaults(); + // nav.down is bound to both "j" and "". + s.apply_overrides([("nav.down", "n")]); + + // Both old bindings should be gone. + use crate::key::{Key as K, KeyCode}; + assert_eq!( + s.lookup(&[K::plain(KeyCode::ArrowDown)]), + LookupResult::NoMatch + ); + } + + #[test] + fn apply_overrides_bad_sequence_is_skipped() { + let mut s = set_with_defaults(); + // "" is not a valid key sequence — the override should be silently skipped. + s.apply_overrides([("nav.down", "")]); + // Original binding is still gone (we removed it before failing to parse). + // This is an acceptable edge case — the user has a bad config. + } +} diff --git a/brittle-keymap/src/defaults.rs b/brittle-keymap/src/defaults.rs new file mode 100644 index 0000000..dc5e7e6 --- /dev/null +++ b/brittle-keymap/src/defaults.rs @@ -0,0 +1,208 @@ +//! Built-in default keybindings. +//! +//! These are Sioyek/zathura/vim-inspired defaults. Every binding refers to an +//! action constant from [`crate::actions`]. + +use crate::{actions as a, binding::BindingSet}; + +/// Return a [`BindingSet`] populated with the built-in default bindings. +/// +/// Multiple key sequences may map to the same action (e.g. both `j` and +/// `` trigger `nav.down`). +pub fn default_bindings() -> BindingSet { + let mut set = BindingSet::new(); + + // Helper to register a binding, panicking if the sequence is invalid. + // Invalid sequences in defaults are a programming error, not a user error. + macro_rules! bind { + ($seq:expr => $action:expr) => { + set.add_parsed($seq, $action) + .unwrap_or_else(|e| panic!("invalid default binding '{}': {}", $seq, e)); + }; + } + + // ── Navigation ──────────────────────────────────────────────────────────── + bind!("j" => a::NAV_DOWN); + bind!("" => a::NAV_DOWN); + bind!("k" => a::NAV_UP); + bind!("" => a::NAV_UP); + bind!("gg" => a::NAV_TOP); + bind!("G" => a::NAV_BOTTOM); + bind!("" => a::NAV_PAGE_DOWN); + bind!("" => a::NAV_PAGE_UP); + bind!("" => a::NAV_PAGE_DOWN); + bind!("" => a::NAV_PAGE_UP); + + // ── Pane focus ──────────────────────────────────────────────────────────── + bind!("" => a::FOCUS_NEXT); + bind!("" => a::FOCUS_PREV); + bind!("H" => a::FOCUS_LEFT); + bind!("M" => a::FOCUS_CENTER); + bind!("L" => a::FOCUS_RIGHT); + + // ── Library tree ────────────────────────────────────────────────────────── + bind!("zo" => a::TREE_EXPAND); + bind!("zc" => a::TREE_COLLAPSE); + bind!("za" => a::TREE_TOGGLE); + // Arrow-style tree navigation: l expands, h collapses. + bind!("l" => a::TREE_EXPAND); + bind!("h" => a::TREE_COLLAPSE); + + // ── Item actions ────────────────────────────────────────────────────────── + bind!("" => a::ACTION_OPEN); + bind!("e" => a::ACTION_EDIT); + bind!("d" => a::ACTION_DELETE); + bind!("n" => a::ACTION_NEW); + + // ── Tabs ────────────────────────────────────────────────────────────────── + // vim-style gt/gT; g is already a prefix from gg (nav.top). + bind!("gt" => a::TAB_NEXT); + bind!("gT" => a::TAB_PREV); + bind!("q" => a::TAB_CLOSE); + + // ── Input modes ─────────────────────────────────────────────────────────── + bind!(":" => a::MODE_COMMAND); + bind!("/" => a::MODE_SEARCH); + bind!("" => a::MODE_NORMAL); + bind!("b" => a::MODE_BIBTEX); + + set +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + binding::LookupResult, + key::{Key, KeyCode}, + }; + + fn defaults() -> BindingSet { + default_bindings() + } + + #[test] + fn j_maps_to_nav_down() { + let d = defaults(); + assert_eq!( + d.lookup(&[Key::char('j')]), + LookupResult::Exact(a::NAV_DOWN.into()) + ); + } + + #[test] + fn arrow_down_maps_to_nav_down() { + let d = defaults(); + assert_eq!( + d.lookup(&[Key::plain(KeyCode::ArrowDown)]), + LookupResult::Exact(a::NAV_DOWN.into()) + ); + } + + #[test] + fn gg_maps_to_nav_top() { + let d = defaults(); + // 'g' alone is a prefix. + assert_eq!(d.lookup(&[Key::char('g')]), LookupResult::Prefix); + // 'gg' is the full binding. + assert_eq!( + d.lookup(&[Key::char('g'), Key::char('g')]), + LookupResult::Exact(a::NAV_TOP.into()) + ); + } + + #[test] + fn capital_g_maps_to_nav_bottom() { + let d = defaults(); + assert_eq!( + d.lookup(&[Key::char('G')]), + LookupResult::Exact(a::NAV_BOTTOM.into()) + ); + } + + #[test] + fn zo_maps_to_tree_expand() { + let d = defaults(); + assert_eq!(d.lookup(&[Key::char('z')]), LookupResult::Prefix); + assert_eq!( + d.lookup(&[Key::char('z'), Key::char('o')]), + LookupResult::Exact(a::TREE_EXPAND.into()) + ); + } + + #[test] + fn colon_maps_to_mode_command() { + let d = defaults(); + assert_eq!( + d.lookup(&[Key::char(':')]), + LookupResult::Exact(a::MODE_COMMAND.into()) + ); + } + + #[test] + fn escape_maps_to_mode_normal() { + let d = defaults(); + assert_eq!( + d.lookup(&[Key::plain(KeyCode::Escape)]), + LookupResult::Exact(a::MODE_NORMAL.into()) + ); + } + + #[test] + fn tab_maps_to_focus_next() { + let d = defaults(); + assert_eq!( + d.lookup(&[Key::plain(KeyCode::Tab)]), + LookupResult::Exact(a::FOCUS_NEXT.into()) + ); + } + + #[test] + fn shift_tab_maps_to_focus_prev() { + use crate::key::parse_sequence; + let d = defaults(); + let shift_tab = &parse_sequence("").unwrap()[0]; + assert_eq!( + d.lookup(&[shift_tab.clone()]), + LookupResult::Exact(a::FOCUS_PREV.into()) + ); + } + + #[test] + fn gt_maps_to_tab_next() { + let d = defaults(); + // g is still a prefix (gg, gt, gT all share it) + assert_eq!(d.lookup(&[Key::char('g')]), LookupResult::Prefix); + assert_eq!( + d.lookup(&[Key::char('g'), Key::char('t')]), + LookupResult::Exact(a::TAB_NEXT.into()) + ); + } + + #[test] + fn capital_gt_maps_to_tab_prev() { + let d = defaults(); + assert_eq!( + d.lookup(&[Key::char('g'), Key::char('T')]), + LookupResult::Exact(a::TAB_PREV.into()) + ); + } + + #[test] + fn q_maps_to_tab_close() { + let d = defaults(); + assert_eq!( + d.lookup(&[Key::char('q')]), + LookupResult::Exact(a::TAB_CLOSE.into()) + ); + } + + #[test] + fn all_default_sequences_are_valid() { + // Constructing the defaults panics on any invalid sequence, so this + // test implicitly validates every binding in default_bindings(). + let _ = default_bindings(); + } +} diff --git a/brittle-keymap/src/key.rs b/brittle-keymap/src/key.rs new file mode 100644 index 0000000..a17662c --- /dev/null +++ b/brittle-keymap/src/key.rs @@ -0,0 +1,371 @@ +//! Key representation and string parsing. +//! +//! Key sequences are written in a vim-inspired notation: +//! +//! | Notation | Meaning | +//! |------------------|-------------------------------| +//! | `j` | the letter j | +//! | `G` | capital G (no modifier needed) | +//! | `` / `` | Enter / Return | +//! | `` | Escape | +//! | `` | Tab | +//! | `` | Backspace | +//! | `` | Delete | +//! | `` | Space bar | +//! | `` | Arrow keys | +//! | `` / `` | Home / End | +//! | `` / `` | Page Up / Down | +//! | `` | Ctrl+x | +//! | `` | Shift+Tab | +//! | `` / `` | Alt+x | +//! +//! Sequences are formed by concatenating specs: `gg`, `zo`, ``. + +use std::fmt; + +/// A single key press, including its modifiers. +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +pub struct Key { + pub code: KeyCode, + pub ctrl: bool, + pub shift: bool, + pub alt: bool, + pub meta: bool, +} + +impl Key { + /// Create an unmodified character key. + pub fn char(c: char) -> Self { + Key { + code: KeyCode::Char(c), + ctrl: false, + shift: false, + alt: false, + meta: false, + } + } + + /// Create a key with the given code and no modifiers. + pub fn plain(code: KeyCode) -> Self { + Key { + code, + ctrl: false, + shift: false, + alt: false, + meta: false, + } + } +} + +impl fmt::Display for Key { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let needs_brackets = self.ctrl + || self.shift + || self.alt + || self.meta + || !matches!(self.code, KeyCode::Char(_)); + + if needs_brackets { + write!(f, "<")?; + if self.ctrl { + write!(f, "C-")?; + } + if self.shift { + write!(f, "S-")?; + } + if self.alt { + write!(f, "M-")?; + } + if self.meta { + write!(f, "D-")?; + } + write!(f, "{}>", self.code) + } else { + write!(f, "{}", self.code) + } + } +} + +/// The logical key code, independent of platform. +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +pub enum KeyCode { + /// A Unicode character key (letters, digits, punctuation). + Char(char), + Enter, + Escape, + Tab, + Backspace, + Delete, + Space, + ArrowUp, + ArrowDown, + ArrowLeft, + ArrowRight, + Home, + End, + PageUp, + PageDown, + F(u8), +} + +impl fmt::Display for KeyCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + KeyCode::Char(c) => write!(f, "{}", c), + KeyCode::Enter => write!(f, "Enter"), + KeyCode::Escape => write!(f, "Esc"), + KeyCode::Tab => write!(f, "Tab"), + KeyCode::Backspace => write!(f, "BS"), + KeyCode::Delete => write!(f, "Del"), + KeyCode::Space => write!(f, "Space"), + KeyCode::ArrowUp => write!(f, "Up"), + KeyCode::ArrowDown => write!(f, "Down"), + KeyCode::ArrowLeft => write!(f, "Left"), + KeyCode::ArrowRight => write!(f, "Right"), + KeyCode::Home => write!(f, "Home"), + KeyCode::End => write!(f, "End"), + KeyCode::PageUp => write!(f, "PageUp"), + KeyCode::PageDown => write!(f, "PageDown"), + KeyCode::F(n) => write!(f, "F{}", n), + } + } +} + +// ── Parsing ─────────────────────────────────────────────────────────────────── + +/// Error type for key sequence parsing. +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum ParseError { + /// A `<` was not closed with a `>`. + UnclosedBracket, + /// A `<...>` block contained an unrecognized key name. + UnknownKey(String), + /// An empty `<>` was encountered. + EmptyBracket, +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ParseError::UnclosedBracket => write!(f, "unclosed '<' in key sequence"), + ParseError::UnknownKey(k) => write!(f, "unknown key name: '{}'", k), + ParseError::EmptyBracket => write!(f, "empty '<>' in key sequence"), + } + } +} + +/// Parse a key sequence string (e.g. `"gg"`, `""`, `"zo"`) into a list of [`Key`]s. +pub fn parse_sequence(s: &str) -> Result, ParseError> { + let mut keys = Vec::new(); + let mut chars = s.chars().peekable(); + + while let Some(c) = chars.next() { + if c == '<' { + // Consume until the matching '>'. + let mut spec = String::new(); + loop { + match chars.next() { + Some('>') => break, + Some(c) => spec.push(c), + None => return Err(ParseError::UnclosedBracket), + } + } + if spec.is_empty() { + return Err(ParseError::EmptyBracket); + } + keys.push(parse_bracket_spec(&spec)?); + } else { + keys.push(Key::char(c)); + } + } + + Ok(keys) +} + +/// Parse the interior of a `<...>` bracket, e.g. `"C-d"`, `"S-Tab"`, `"Enter"`. +fn parse_bracket_spec(spec: &str) -> Result { + let mut ctrl = false; + let mut shift = false; + let mut alt = false; + let mut meta = false; + let mut rest = spec; + + // Strip modifier prefixes in any order. + loop { + if let Some(s) = rest.strip_prefix("C-") { + ctrl = true; + rest = s; + } else if let Some(s) = rest.strip_prefix("S-") { + shift = true; + rest = s; + } else if let Some(s) = rest.strip_prefix("M-").or_else(|| rest.strip_prefix("A-")) { + alt = true; + rest = s; + } else if let Some(s) = rest.strip_prefix("D-") { + meta = true; + rest = s; + } else { + break; + } + } + + let code = match rest { + "Enter" | "CR" | "Return" => KeyCode::Enter, + "Esc" | "Escape" => KeyCode::Escape, + "Tab" => KeyCode::Tab, + "BS" | "Backspace" => KeyCode::Backspace, + "Del" | "Delete" => KeyCode::Delete, + "Space" => KeyCode::Space, + "Up" => KeyCode::ArrowUp, + "Down" => KeyCode::ArrowDown, + "Left" => KeyCode::ArrowLeft, + "Right" => KeyCode::ArrowRight, + "Home" => KeyCode::Home, + "End" => KeyCode::End, + "PageUp" => KeyCode::PageUp, + "PageDown" => KeyCode::PageDown, + s if s.starts_with('F') && s.len() > 1 => { + let n: u8 = s[1..] + .parse() + .map_err(|_| ParseError::UnknownKey(spec.to_owned()))?; + KeyCode::F(n) + } + s if s.chars().count() == 1 => KeyCode::Char(s.chars().next().unwrap()), + _ => return Err(ParseError::UnknownKey(spec.to_owned())), + }; + + Ok(Key { + code, + ctrl, + shift, + alt, + meta, + }) +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + + fn seq(s: &str) -> Vec { + parse_sequence(s).unwrap() + } + + #[test] + fn single_char() { + assert_eq!(seq("j"), vec![Key::char('j')]); + assert_eq!(seq("G"), vec![Key::char('G')]); + } + + #[test] + fn multi_char_sequence() { + assert_eq!(seq("gg"), vec![Key::char('g'), Key::char('g')]); + assert_eq!(seq("zo"), vec![Key::char('z'), Key::char('o')]); + } + + #[test] + fn special_keys() { + assert_eq!(seq(""), vec![Key::plain(KeyCode::Enter)]); + assert_eq!(seq(""), vec![Key::plain(KeyCode::Enter)]); + assert_eq!(seq(""), vec![Key::plain(KeyCode::Escape)]); + assert_eq!(seq(""), vec![Key::plain(KeyCode::Tab)]); + assert_eq!(seq(""), vec![Key::plain(KeyCode::Backspace)]); + assert_eq!(seq(""), vec![Key::plain(KeyCode::Delete)]); + assert_eq!(seq(""), vec![Key::plain(KeyCode::Space)]); + assert_eq!(seq(""), vec![Key::plain(KeyCode::ArrowUp)]); + assert_eq!(seq(""), vec![Key::plain(KeyCode::ArrowDown)]); + assert_eq!(seq(""), vec![Key::plain(KeyCode::Home)]); + assert_eq!(seq(""), vec![Key::plain(KeyCode::End)]); + assert_eq!(seq(""), vec![Key::plain(KeyCode::PageUp)]); + assert_eq!(seq(""), vec![Key::plain(KeyCode::PageDown)]); + } + + #[test] + fn ctrl_modifier() { + let key = &seq("")[0]; + assert_eq!(key.code, KeyCode::Char('d')); + assert!(key.ctrl); + assert!(!key.shift); + } + + #[test] + fn shift_modifier() { + let key = &seq("")[0]; + assert_eq!(key.code, KeyCode::Tab); + assert!(key.shift); + } + + #[test] + fn alt_modifier() { + let key = &seq("")[0]; + assert_eq!(key.code, KeyCode::Char('j')); + assert!(key.alt); + } + + #[test] + fn combined_modifiers() { + let key = &seq("")[0]; + assert_eq!(key.code, KeyCode::Tab); + assert!(key.ctrl); + assert!(key.shift); + } + + #[test] + fn function_key() { + assert_eq!(seq(""), vec![Key::plain(KeyCode::F(1))]); + assert_eq!(seq(""), vec![Key::plain(KeyCode::F(12))]); + } + + #[test] + fn mixed_sequence() { + let keys = seq("j"); + assert_eq!(keys.len(), 3); + assert_eq!(keys[0].code, KeyCode::Char('d')); + assert!(keys[0].ctrl); + assert_eq!(keys[1], Key::char('j')); + assert_eq!(keys[2].code, KeyCode::Enter); + } + + #[test] + fn unclosed_bracket_error() { + assert_eq!(parse_sequence(""), + Err(ParseError::UnknownKey(_)) + )); + } + + #[test] + fn empty_bracket_error() { + assert_eq!(parse_sequence("<>"), Err(ParseError::EmptyBracket)); + } + + #[test] + fn display_char_key() { + assert_eq!(Key::char('j').to_string(), "j"); + } + + #[test] + fn display_ctrl_key() { + let k = seq("")[0].clone(); + assert_eq!(k.to_string(), ""); + } + + #[test] + fn display_shift_tab() { + let k = seq("")[0].clone(); + assert_eq!(k.to_string(), ""); + } + + #[test] + fn display_special_key() { + let k = Key::plain(KeyCode::Enter); + assert_eq!(k.to_string(), ""); + } +} diff --git a/brittle-keymap/src/lib.rs b/brittle-keymap/src/lib.rs new file mode 100644 index 0000000..efed64e --- /dev/null +++ b/brittle-keymap/src/lib.rs @@ -0,0 +1,23 @@ +//! Keymap engine for Brittle. +//! +//! Provides vim-style key sequence parsing, binding sets, and a stateful +//! input processor that supports count prefixes and multi-key sequences. +//! +//! # Quick start +//! +//! ```rust +//! use brittle_keymap::{KeymapState, default_bindings}; +//! +//! let mut state = KeymapState::new(default_bindings()); +//! ``` + +pub mod actions; +pub mod binding; +pub mod defaults; +pub mod key; +pub mod state; + +pub use binding::{BindingSet, LookupResult}; +pub use defaults::default_bindings; +pub use key::{parse_sequence, Key, KeyCode, ParseError}; +pub use state::{KeymapState, Outcome}; diff --git a/brittle-keymap/src/state.rs b/brittle-keymap/src/state.rs new file mode 100644 index 0000000..5e20716 --- /dev/null +++ b/brittle-keymap/src/state.rs @@ -0,0 +1,354 @@ +//! The keymap state machine. +//! +//! `KeymapState` processes a stream of [`Key`] presses and emits [`Outcome`]s. +//! It supports: +//! +//! - **Count prefixes**: digits build a numeric multiplier before the binding +//! fires (e.g. `5j` → `nav.down` with count 5). `0` alone is treated as a +//! regular key (not a count) so it can be bound; `10`, `20`, … work normally. +//! +//! - **Multi-key sequences**: bindings like `gg`, `zo`, `zc` are resolved by +//! accumulating pressed keys until an exact match is found. If the +//! accumulated sequence stops being a prefix of any binding, the machine +//! discards the prefix and retries with just the latest key. +//! +//! - **Configurable bindings**: the `BindingSet` is injected at construction; +//! user overrides are applied before constructing the state. + +use crate::{ + binding::{BindingSet, LookupResult}, + key::Key, +}; + +/// What the state machine decided after processing one key press. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Outcome { + /// An action was fully resolved. + Action { + /// The action name (see [`crate::actions`]). + name: String, + /// The count prefix (always ≥ 1; `1` if no prefix was typed). + count: u32, + }, + /// The key was consumed but we are waiting for more keys. + Pending, + /// The key (or the accumulated sequence) did not match any binding. + Unbound, +} + +/// The keymap state machine. +#[derive(Debug)] +pub struct KeymapState { + bindings: BindingSet, + /// Keys accumulated so far in the current multi-key sequence. + pending: Vec, + /// The count prefix typed before the current sequence (0 = none). + count: u32, +} + +impl KeymapState { + pub fn new(bindings: BindingSet) -> Self { + Self { + bindings, + pending: Vec::new(), + count: 0, + } + } + + /// Process one key press and return the outcome. + pub fn process(&mut self, key: Key) -> Outcome { + // Count-building: only when no sequence is in progress. + if self.pending.is_empty() { + if let Some(digit) = digit_of(&key) { + // '0' alone is not a count-start — it's treated as a key. + // Once a count > 0 has started, '0' extends it (e.g., "10j"). + if self.count > 0 || digit != 0 { + self.count = self.count.saturating_mul(10).saturating_add(digit); + return Outcome::Pending; + } + } + } + + // Append to the in-progress sequence and look it up. + self.pending.push(key.clone()); + + match self.bindings.lookup(&self.pending) { + LookupResult::Exact(action) => { + let count = if self.count == 0 { 1 } else { self.count }; + let outcome = Outcome::Action { + name: action, + count, + }; + self.reset(); + outcome + } + LookupResult::Prefix => Outcome::Pending, + LookupResult::NoMatch => { + // The accumulated sequence is a dead end. + // Discard the prefix and retry with only the last key, + // unless this is already a single-key sequence. + if self.pending.len() > 1 { + let last = key; // the key we just pushed + self.pending.clear(); + self.count = 0; + return self.process(last); + } + // Single key and still no match → unbound. + self.reset(); + Outcome::Unbound + } + } + } + + /// Reset the state machine to idle (clears pending keys and count). + pub fn reset(&mut self) { + self.pending.clear(); + self.count = 0; + } + + /// Keys accumulated so far (useful for displaying a "pending sequence" hint). + pub fn pending_keys(&self) -> &[Key] { + &self.pending + } + + /// Count prefix typed so far (0 = none). + pub fn current_count(&self) -> u32 { + self.count + } +} + +/// If `key` is an unmodified digit, return its numeric value; otherwise `None`. +fn digit_of(key: &Key) -> Option { + if key.ctrl || key.shift || key.alt || key.meta { + return None; + } + if let crate::key::KeyCode::Char(c) = key.code { + c.to_digit(10) + } else { + None + } +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use crate::key::{Key, KeyCode}; + + fn make_state() -> KeymapState { + let mut bindings = BindingSet::new(); + bindings.add_parsed("j", "nav.down").unwrap(); + bindings.add_parsed("k", "nav.up").unwrap(); + bindings.add_parsed("", "nav.down").unwrap(); + bindings.add_parsed("gg", "nav.top").unwrap(); + bindings.add_parsed("G", "nav.bottom").unwrap(); + bindings.add_parsed("zo", "tree.expand").unwrap(); + bindings.add_parsed("zc", "tree.collapse").unwrap(); + bindings.add_parsed("za", "tree.toggle").unwrap(); + bindings.add_parsed("", "mode.normal").unwrap(); + bindings.add_parsed(":", "mode.command").unwrap(); + KeymapState::new(bindings) + } + + fn key(c: char) -> Key { + Key::char(c) + } + + fn special(code: KeyCode) -> Key { + Key::plain(code) + } + + // ── Single-key bindings ─────────────────────────────────────────────────── + + #[test] + fn single_key_fires_action() { + let mut s = make_state(); + assert_eq!( + s.process(key('j')), + Outcome::Action { + name: "nav.down".into(), + count: 1 + } + ); + } + + #[test] + fn unbound_key_returns_unbound() { + let mut s = make_state(); + assert_eq!(s.process(key('x')), Outcome::Unbound); + } + + #[test] + fn special_key_fires_action() { + let mut s = make_state(); + assert_eq!( + s.process(special(KeyCode::Escape)), + Outcome::Action { + name: "mode.normal".into(), + count: 1 + } + ); + } + + // ── Count prefix ────────────────────────────────────────────────────────── + + #[test] + fn count_prefix_single_digit() { + let mut s = make_state(); + assert_eq!(s.process(key('5')), Outcome::Pending); + assert_eq!(s.current_count(), 5); + assert_eq!( + s.process(key('j')), + Outcome::Action { + name: "nav.down".into(), + count: 5 + } + ); + } + + #[test] + fn count_prefix_multi_digit() { + let mut s = make_state(); + assert_eq!(s.process(key('1')), Outcome::Pending); + assert_eq!(s.process(key('0')), Outcome::Pending); + assert_eq!(s.current_count(), 10); + assert_eq!( + s.process(key('j')), + Outcome::Action { + name: "nav.down".into(), + count: 10 + } + ); + } + + #[test] + fn zero_alone_is_not_a_count() { + // '0' alone with no prior count should be treated as a key, not a count-start. + let mut bindings = BindingSet::new(); + bindings.add_parsed("0", "nav.top").unwrap(); + let mut s = KeymapState::new(bindings); + assert_eq!( + s.process(key('0')), + Outcome::Action { + name: "nav.top".into(), + count: 1 + } + ); + } + + #[test] + fn count_resets_after_action() { + let mut s = make_state(); + s.process(key('3')); + s.process(key('j')); + // Next key should have count=1 again. + assert_eq!( + s.process(key('j')), + Outcome::Action { + name: "nav.down".into(), + count: 1 + } + ); + } + + // ── Multi-key sequences ─────────────────────────────────────────────────── + + #[test] + fn multi_key_first_key_is_pending() { + let mut s = make_state(); + assert_eq!(s.process(key('g')), Outcome::Pending); + assert_eq!(s.pending_keys(), &[Key::char('g')]); + } + + #[test] + fn multi_key_sequence_fires_on_completion() { + let mut s = make_state(); + s.process(key('g')); + assert_eq!( + s.process(key('g')), + Outcome::Action { + name: "nav.top".into(), + count: 1 + } + ); + // State is cleared after action. + assert!(s.pending_keys().is_empty()); + } + + #[test] + fn multi_key_wrong_continuation_retries_last_key() { + let mut s = make_state(); + // 'g' starts a sequence; 'j' does not continue it → retry 'j' alone. + s.process(key('g')); + assert_eq!( + s.process(key('j')), + Outcome::Action { + name: "nav.down".into(), + count: 1 + } + ); + } + + #[test] + fn multi_key_wrong_continuation_unbound_if_retry_also_fails() { + let mut s = make_state(); + // 'g' starts a sequence; 'x' does not continue it and is also unbound alone. + s.process(key('g')); + assert_eq!(s.process(key('x')), Outcome::Unbound); + } + + #[test] + fn zo_sequence() { + let mut s = make_state(); + assert_eq!(s.process(key('z')), Outcome::Pending); + assert_eq!( + s.process(key('o')), + Outcome::Action { + name: "tree.expand".into(), + count: 1 + } + ); + } + + #[test] + fn count_with_multi_key_sequence() { + let mut s = make_state(); + s.process(key('3')); // count = 3 + s.process(key('z')); // pending = [z] + assert_eq!( + s.process(key('o')), + Outcome::Action { + name: "tree.expand".into(), + count: 3 + } + ); + } + + // ── Reset ───────────────────────────────────────────────────────────────── + + #[test] + fn reset_clears_pending_and_count() { + let mut s = make_state(); + s.process(key('5')); + s.process(key('g')); + s.reset(); + assert_eq!(s.current_count(), 0); + assert!(s.pending_keys().is_empty()); + } + + #[test] + fn after_reset_processes_normally() { + let mut s = make_state(); + s.process(key('5')); + s.process(key('g')); + s.reset(); + assert_eq!( + s.process(key('j')), + Outcome::Action { + name: "nav.down".into(), + count: 1 + } + ); + } +} diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..f392ad3 --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "brittle-app" +version = "0.1.0" +edition = "2021" + +[lib] +name = "brittle_app" + +[[bin]] +name = "brittle" +path = "src/main.rs" + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +brittle-core = { path = "../brittle-core" } +tauri = { version = "2", features = [] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +toml = "0.8" +dirs = "6" +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +thiserror = "2" +url = "2" +urlencoding = "2" +uuid = { version = "1", features = ["v7"] } + +[dev-dependencies] +tempfile = "3" diff --git a/src-tauri/Trunk.toml b/src-tauri/Trunk.toml new file mode 100644 index 0000000..4a3c0b1 --- /dev/null +++ b/src-tauri/Trunk.toml @@ -0,0 +1,7 @@ +[build] +target = "../src/index.html" +dist = "../dist" + +[serve] +port = 1420 +open = false diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 0000000..fdbe2a9 --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://schema.tauri.app/config/2/capability.json", + "identifier": "default", + "description": "Default capability for the main window", + "windows": ["main"], + "permissions": ["core:default"] +} diff --git a/src-tauri/gen/schemas/acl-manifests.json b/src-tauri/gen/schemas/acl-manifests.json new file mode 100644 index 0000000..43da9ef --- /dev/null +++ b/src-tauri/gen/schemas/acl-manifests.json @@ -0,0 +1 @@ +{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file diff --git a/src-tauri/gen/schemas/capabilities.json b/src-tauri/gen/schemas/capabilities.json new file mode 100644 index 0000000..26bf4b4 --- /dev/null +++ b/src-tauri/gen/schemas/capabilities.json @@ -0,0 +1 @@ +{"default":{"identifier":"default","description":"Default capability for the main window","local":true,"windows":["main"],"permissions":["core:default"]}} \ No newline at end of file diff --git a/src-tauri/gen/schemas/desktop-schema.json b/src-tauri/gen/schemas/desktop-schema.json new file mode 100644 index 0000000..260dbe0 --- /dev/null +++ b/src-tauri/gen/schemas/desktop-schema.json @@ -0,0 +1,2244 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CapabilityFile", + "description": "Capability formats accepted in a capability file.", + "anyOf": [ + { + "description": "A single capability.", + "allOf": [ + { + "$ref": "#/definitions/Capability" + } + ] + }, + { + "description": "A list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + }, + { + "description": "A list of capabilities.", + "type": "object", + "required": [ + "capabilities" + ], + "properties": { + "capabilities": { + "description": "The list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + } + } + } + ], + "definitions": { + "Capability": { + "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", + "type": "object", + "required": [ + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", + "type": "string" + }, + "description": { + "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", + "default": "", + "type": "string" + }, + "remote": { + "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", + "anyOf": [ + { + "$ref": "#/definitions/CapabilityRemote" + }, + { + "type": "null" + } + ] + }, + "local": { + "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", + "default": true, + "type": "boolean" + }, + "windows": { + "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "webviews": { + "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "permissions": { + "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionEntry" + }, + "uniqueItems": true + }, + "platforms": { + "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "CapabilityRemote": { + "description": "Configuration for remote URLs that are associated with the capability.", + "type": "object", + "required": [ + "urls" + ], + "properties": { + "urls": { + "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionEntry": { + "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", + "anyOf": [ + { + "description": "Reference a permission or permission set by identifier.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + { + "description": "Reference a permission or permission set by identifier and extends its scope.", + "type": "object", + "allOf": [ + { + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + } + ], + "required": [ + "identifier" + ] + } + ] + }, + "Identifier": { + "description": "Permission identifier", + "oneOf": [ + { + "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", + "type": "string", + "const": "core:default", + "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`", + "type": "string", + "const": "core:app:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`" + }, + { + "description": "Enables the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-hide", + "markdownDescription": "Enables the app_hide command without any pre-configured scope." + }, + { + "description": "Enables the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-show", + "markdownDescription": "Enables the app_show command without any pre-configured scope." + }, + { + "description": "Enables the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-bundle-type", + "markdownDescription": "Enables the bundle_type command without any pre-configured scope." + }, + { + "description": "Enables the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-default-window-icon", + "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." + }, + { + "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-fetch-data-store-identifiers", + "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Enables the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-identifier", + "markdownDescription": "Enables the identifier command without any pre-configured scope." + }, + { + "description": "Enables the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-name", + "markdownDescription": "Enables the name command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-data-store", + "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." + }, + { + "description": "Enables the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-listener", + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." + }, + { + "description": "Enables the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-app-theme", + "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-dock-visibility", + "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Enables the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-tauri-version", + "markdownDescription": "Enables the tauri_version command without any pre-configured scope." + }, + { + "description": "Enables the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-version", + "markdownDescription": "Enables the version command without any pre-configured scope." + }, + { + "description": "Denies the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-hide", + "markdownDescription": "Denies the app_hide command without any pre-configured scope." + }, + { + "description": "Denies the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-show", + "markdownDescription": "Denies the app_show command without any pre-configured scope." + }, + { + "description": "Denies the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-bundle-type", + "markdownDescription": "Denies the bundle_type command without any pre-configured scope." + }, + { + "description": "Denies the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-default-window-icon", + "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." + }, + { + "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-fetch-data-store-identifiers", + "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Denies the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-identifier", + "markdownDescription": "Denies the identifier command without any pre-configured scope." + }, + { + "description": "Denies the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-name", + "markdownDescription": "Denies the name command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-data-store", + "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." + }, + { + "description": "Denies the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-listener", + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." + }, + { + "description": "Denies the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-app-theme", + "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-dock-visibility", + "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Denies the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-tauri-version", + "markdownDescription": "Denies the tauri_version command without any pre-configured scope." + }, + { + "description": "Denies the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-version", + "markdownDescription": "Denies the version command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", + "type": "string", + "const": "core:event:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" + }, + { + "description": "Enables the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit", + "markdownDescription": "Enables the emit command without any pre-configured scope." + }, + { + "description": "Enables the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit-to", + "markdownDescription": "Enables the emit_to command without any pre-configured scope." + }, + { + "description": "Enables the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-listen", + "markdownDescription": "Enables the listen command without any pre-configured scope." + }, + { + "description": "Enables the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-unlisten", + "markdownDescription": "Enables the unlisten command without any pre-configured scope." + }, + { + "description": "Denies the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit", + "markdownDescription": "Denies the emit command without any pre-configured scope." + }, + { + "description": "Denies the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit-to", + "markdownDescription": "Denies the emit_to command without any pre-configured scope." + }, + { + "description": "Denies the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-listen", + "markdownDescription": "Denies the listen command without any pre-configured scope." + }, + { + "description": "Denies the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-unlisten", + "markdownDescription": "Denies the unlisten command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", + "type": "string", + "const": "core:image:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" + }, + { + "description": "Enables the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-bytes", + "markdownDescription": "Enables the from_bytes command without any pre-configured scope." + }, + { + "description": "Enables the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-path", + "markdownDescription": "Enables the from_path command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-rgba", + "markdownDescription": "Enables the rgba command without any pre-configured scope." + }, + { + "description": "Enables the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-size", + "markdownDescription": "Enables the size command without any pre-configured scope." + }, + { + "description": "Denies the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-bytes", + "markdownDescription": "Denies the from_bytes command without any pre-configured scope." + }, + { + "description": "Denies the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-path", + "markdownDescription": "Denies the from_path command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-rgba", + "markdownDescription": "Denies the rgba command without any pre-configured scope." + }, + { + "description": "Denies the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-size", + "markdownDescription": "Denies the size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", + "type": "string", + "const": "core:menu:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" + }, + { + "description": "Enables the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-append", + "markdownDescription": "Enables the append command without any pre-configured scope." + }, + { + "description": "Enables the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-create-default", + "markdownDescription": "Enables the create_default command without any pre-configured scope." + }, + { + "description": "Enables the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-get", + "markdownDescription": "Enables the get command without any pre-configured scope." + }, + { + "description": "Enables the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-insert", + "markdownDescription": "Enables the insert command without any pre-configured scope." + }, + { + "description": "Enables the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-checked", + "markdownDescription": "Enables the is_checked command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-items", + "markdownDescription": "Enables the items command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-popup", + "markdownDescription": "Enables the popup command without any pre-configured scope." + }, + { + "description": "Enables the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-prepend", + "markdownDescription": "Enables the prepend command without any pre-configured scope." + }, + { + "description": "Enables the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove", + "markdownDescription": "Enables the remove command without any pre-configured scope." + }, + { + "description": "Enables the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove-at", + "markdownDescription": "Enables the remove_at command without any pre-configured scope." + }, + { + "description": "Enables the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-accelerator", + "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." + }, + { + "description": "Enables the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-app-menu", + "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-help-menu-for-nsapp", + "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-window-menu", + "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-windows-menu-for-nsapp", + "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-checked", + "markdownDescription": "Enables the set_checked command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-text", + "markdownDescription": "Enables the set_text command without any pre-configured scope." + }, + { + "description": "Enables the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-text", + "markdownDescription": "Enables the text command without any pre-configured scope." + }, + { + "description": "Denies the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-append", + "markdownDescription": "Denies the append command without any pre-configured scope." + }, + { + "description": "Denies the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-create-default", + "markdownDescription": "Denies the create_default command without any pre-configured scope." + }, + { + "description": "Denies the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-get", + "markdownDescription": "Denies the get command without any pre-configured scope." + }, + { + "description": "Denies the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-insert", + "markdownDescription": "Denies the insert command without any pre-configured scope." + }, + { + "description": "Denies the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-checked", + "markdownDescription": "Denies the is_checked command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-items", + "markdownDescription": "Denies the items command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-popup", + "markdownDescription": "Denies the popup command without any pre-configured scope." + }, + { + "description": "Denies the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-prepend", + "markdownDescription": "Denies the prepend command without any pre-configured scope." + }, + { + "description": "Denies the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove", + "markdownDescription": "Denies the remove command without any pre-configured scope." + }, + { + "description": "Denies the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove-at", + "markdownDescription": "Denies the remove_at command without any pre-configured scope." + }, + { + "description": "Denies the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-accelerator", + "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." + }, + { + "description": "Denies the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-app-menu", + "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-help-menu-for-nsapp", + "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-window-menu", + "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-windows-menu-for-nsapp", + "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-checked", + "markdownDescription": "Denies the set_checked command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-text", + "markdownDescription": "Denies the set_text command without any pre-configured scope." + }, + { + "description": "Denies the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-text", + "markdownDescription": "Denies the text command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", + "type": "string", + "const": "core:path:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" + }, + { + "description": "Enables the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-basename", + "markdownDescription": "Enables the basename command without any pre-configured scope." + }, + { + "description": "Enables the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-dirname", + "markdownDescription": "Enables the dirname command without any pre-configured scope." + }, + { + "description": "Enables the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-extname", + "markdownDescription": "Enables the extname command without any pre-configured scope." + }, + { + "description": "Enables the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-is-absolute", + "markdownDescription": "Enables the is_absolute command without any pre-configured scope." + }, + { + "description": "Enables the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-join", + "markdownDescription": "Enables the join command without any pre-configured scope." + }, + { + "description": "Enables the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-normalize", + "markdownDescription": "Enables the normalize command without any pre-configured scope." + }, + { + "description": "Enables the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve", + "markdownDescription": "Enables the resolve command without any pre-configured scope." + }, + { + "description": "Enables the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve-directory", + "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." + }, + { + "description": "Denies the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-basename", + "markdownDescription": "Denies the basename command without any pre-configured scope." + }, + { + "description": "Denies the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-dirname", + "markdownDescription": "Denies the dirname command without any pre-configured scope." + }, + { + "description": "Denies the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-extname", + "markdownDescription": "Denies the extname command without any pre-configured scope." + }, + { + "description": "Denies the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-is-absolute", + "markdownDescription": "Denies the is_absolute command without any pre-configured scope." + }, + { + "description": "Denies the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-join", + "markdownDescription": "Denies the join command without any pre-configured scope." + }, + { + "description": "Denies the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-normalize", + "markdownDescription": "Denies the normalize command without any pre-configured scope." + }, + { + "description": "Denies the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve", + "markdownDescription": "Denies the resolve command without any pre-configured scope." + }, + { + "description": "Denies the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve-directory", + "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", + "type": "string", + "const": "core:resources:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`", + "type": "string", + "const": "core:tray:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`" + }, + { + "description": "Enables the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-get-by-id", + "markdownDescription": "Enables the get_by_id command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-remove-by-id", + "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-as-template", + "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-menu", + "markdownDescription": "Enables the set_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-show-menu-on-left-click", + "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Enables the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-temp-dir-path", + "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-tooltip", + "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." + }, + { + "description": "Enables the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-visible", + "markdownDescription": "Enables the set_visible command without any pre-configured scope." + }, + { + "description": "Denies the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-get-by-id", + "markdownDescription": "Denies the get_by_id command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-remove-by-id", + "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-as-template", + "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-menu", + "markdownDescription": "Denies the set_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-show-menu-on-left-click", + "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Denies the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-temp-dir-path", + "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-tooltip", + "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." + }, + { + "description": "Denies the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-visible", + "markdownDescription": "Denies the set_visible command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", + "type": "string", + "const": "core:webview:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" + }, + { + "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-clear-all-browsing-data", + "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Enables the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview", + "markdownDescription": "Enables the create_webview command without any pre-configured scope." + }, + { + "description": "Enables the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview-window", + "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." + }, + { + "description": "Enables the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-get-all-webviews", + "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-internal-toggle-devtools", + "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Enables the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-print", + "markdownDescription": "Enables the print command without any pre-configured scope." + }, + { + "description": "Enables the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-reparent", + "markdownDescription": "Enables the reparent command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-auto-resize", + "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-background-color", + "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-focus", + "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-position", + "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-size", + "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-zoom", + "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Enables the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-close", + "markdownDescription": "Enables the webview_close command without any pre-configured scope." + }, + { + "description": "Enables the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-hide", + "markdownDescription": "Enables the webview_hide command without any pre-configured scope." + }, + { + "description": "Enables the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-position", + "markdownDescription": "Enables the webview_position command without any pre-configured scope." + }, + { + "description": "Enables the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-show", + "markdownDescription": "Enables the webview_show command without any pre-configured scope." + }, + { + "description": "Enables the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-size", + "markdownDescription": "Enables the webview_size command without any pre-configured scope." + }, + { + "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-clear-all-browsing-data", + "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Denies the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview", + "markdownDescription": "Denies the create_webview command without any pre-configured scope." + }, + { + "description": "Denies the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview-window", + "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." + }, + { + "description": "Denies the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-get-all-webviews", + "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-internal-toggle-devtools", + "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Denies the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-print", + "markdownDescription": "Denies the print command without any pre-configured scope." + }, + { + "description": "Denies the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-reparent", + "markdownDescription": "Denies the reparent command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-auto-resize", + "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-background-color", + "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-focus", + "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-position", + "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-size", + "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-zoom", + "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Denies the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-close", + "markdownDescription": "Denies the webview_close command without any pre-configured scope." + }, + { + "description": "Denies the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-hide", + "markdownDescription": "Denies the webview_hide command without any pre-configured scope." + }, + { + "description": "Denies the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-position", + "markdownDescription": "Denies the webview_position command without any pre-configured scope." + }, + { + "description": "Denies the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-show", + "markdownDescription": "Denies the webview_show command without any pre-configured scope." + }, + { + "description": "Denies the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-size", + "markdownDescription": "Denies the webview_size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`", + "type": "string", + "const": "core:window:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`" + }, + { + "description": "Enables the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-available-monitors", + "markdownDescription": "Enables the available_monitors command without any pre-configured scope." + }, + { + "description": "Enables the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-center", + "markdownDescription": "Enables the center command without any pre-configured scope." + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Enables the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-create", + "markdownDescription": "Enables the create command without any pre-configured scope." + }, + { + "description": "Enables the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-current-monitor", + "markdownDescription": "Enables the current_monitor command without any pre-configured scope." + }, + { + "description": "Enables the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-cursor-position", + "markdownDescription": "Enables the cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-destroy", + "markdownDescription": "Enables the destroy command without any pre-configured scope." + }, + { + "description": "Enables the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-get-all-windows", + "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." + }, + { + "description": "Enables the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-hide", + "markdownDescription": "Enables the hide command without any pre-configured scope." + }, + { + "description": "Enables the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-position", + "markdownDescription": "Enables the inner_position command without any pre-configured scope." + }, + { + "description": "Enables the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-size", + "markdownDescription": "Enables the inner_size command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-internal-toggle-maximize", + "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-always-on-top", + "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-closable", + "markdownDescription": "Enables the is_closable command without any pre-configured scope." + }, + { + "description": "Enables the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-decorated", + "markdownDescription": "Enables the is_decorated command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-focused", + "markdownDescription": "Enables the is_focused command without any pre-configured scope." + }, + { + "description": "Enables the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-fullscreen", + "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximizable", + "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximized", + "markdownDescription": "Enables the is_maximized command without any pre-configured scope." + }, + { + "description": "Enables the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimizable", + "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimized", + "markdownDescription": "Enables the is_minimized command without any pre-configured scope." + }, + { + "description": "Enables the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-resizable", + "markdownDescription": "Enables the is_resizable command without any pre-configured scope." + }, + { + "description": "Enables the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-visible", + "markdownDescription": "Enables the is_visible command without any pre-configured scope." + }, + { + "description": "Enables the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-maximize", + "markdownDescription": "Enables the maximize command without any pre-configured scope." + }, + { + "description": "Enables the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-minimize", + "markdownDescription": "Enables the minimize command without any pre-configured scope." + }, + { + "description": "Enables the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-monitor-from-point", + "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Enables the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-position", + "markdownDescription": "Enables the outer_position command without any pre-configured scope." + }, + { + "description": "Enables the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-size", + "markdownDescription": "Enables the outer_size command without any pre-configured scope." + }, + { + "description": "Enables the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-primary-monitor", + "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." + }, + { + "description": "Enables the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-request-user-attention", + "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." + }, + { + "description": "Enables the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scale-factor", + "markdownDescription": "Enables the scale_factor command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-bottom", + "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-top", + "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-background-color", + "markdownDescription": "Enables the set_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-count", + "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-label", + "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." + }, + { + "description": "Enables the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-closable", + "markdownDescription": "Enables the set_closable command without any pre-configured scope." + }, + { + "description": "Enables the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-content-protected", + "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-grab", + "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-icon", + "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-position", + "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-visible", + "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Enables the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-decorations", + "markdownDescription": "Enables the set_decorations command without any pre-configured scope." + }, + { + "description": "Enables the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-effects", + "markdownDescription": "Enables the set_effects command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focus", + "markdownDescription": "Enables the set_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focusable", + "markdownDescription": "Enables the set_focusable command without any pre-configured scope." + }, + { + "description": "Enables the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-fullscreen", + "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-ignore-cursor-events", + "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Enables the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-max-size", + "markdownDescription": "Enables the set_max_size command without any pre-configured scope." + }, + { + "description": "Enables the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-maximizable", + "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-min-size", + "markdownDescription": "Enables the set_min_size command without any pre-configured scope." + }, + { + "description": "Enables the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-minimizable", + "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-overlay-icon", + "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-position", + "markdownDescription": "Enables the set_position command without any pre-configured scope." + }, + { + "description": "Enables the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-progress-bar", + "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Enables the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-resizable", + "markdownDescription": "Enables the set_resizable command without any pre-configured scope." + }, + { + "description": "Enables the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-shadow", + "markdownDescription": "Enables the set_shadow command without any pre-configured scope." + }, + { + "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-simple-fullscreen", + "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size", + "markdownDescription": "Enables the set_size command without any pre-configured scope." + }, + { + "description": "Enables the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size-constraints", + "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Enables the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-skip-taskbar", + "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Enables the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-theme", + "markdownDescription": "Enables the set_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title-bar-style", + "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-visible-on-all-workspaces", + "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Enables the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-dragging", + "markdownDescription": "Enables the start_dragging command without any pre-configured scope." + }, + { + "description": "Enables the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-resize-dragging", + "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Enables the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-theme", + "markdownDescription": "Enables the theme command without any pre-configured scope." + }, + { + "description": "Enables the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-title", + "markdownDescription": "Enables the title command without any pre-configured scope." + }, + { + "description": "Enables the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-toggle-maximize", + "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unmaximize", + "markdownDescription": "Enables the unmaximize command without any pre-configured scope." + }, + { + "description": "Enables the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unminimize", + "markdownDescription": "Enables the unminimize command without any pre-configured scope." + }, + { + "description": "Denies the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-available-monitors", + "markdownDescription": "Denies the available_monitors command without any pre-configured scope." + }, + { + "description": "Denies the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-center", + "markdownDescription": "Denies the center command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Denies the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-create", + "markdownDescription": "Denies the create command without any pre-configured scope." + }, + { + "description": "Denies the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-current-monitor", + "markdownDescription": "Denies the current_monitor command without any pre-configured scope." + }, + { + "description": "Denies the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-cursor-position", + "markdownDescription": "Denies the cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-destroy", + "markdownDescription": "Denies the destroy command without any pre-configured scope." + }, + { + "description": "Denies the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-get-all-windows", + "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." + }, + { + "description": "Denies the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-hide", + "markdownDescription": "Denies the hide command without any pre-configured scope." + }, + { + "description": "Denies the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-position", + "markdownDescription": "Denies the inner_position command without any pre-configured scope." + }, + { + "description": "Denies the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-size", + "markdownDescription": "Denies the inner_size command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-internal-toggle-maximize", + "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-always-on-top", + "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-closable", + "markdownDescription": "Denies the is_closable command without any pre-configured scope." + }, + { + "description": "Denies the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-decorated", + "markdownDescription": "Denies the is_decorated command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-focused", + "markdownDescription": "Denies the is_focused command without any pre-configured scope." + }, + { + "description": "Denies the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-fullscreen", + "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximizable", + "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximized", + "markdownDescription": "Denies the is_maximized command without any pre-configured scope." + }, + { + "description": "Denies the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimizable", + "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimized", + "markdownDescription": "Denies the is_minimized command without any pre-configured scope." + }, + { + "description": "Denies the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-resizable", + "markdownDescription": "Denies the is_resizable command without any pre-configured scope." + }, + { + "description": "Denies the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-visible", + "markdownDescription": "Denies the is_visible command without any pre-configured scope." + }, + { + "description": "Denies the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-maximize", + "markdownDescription": "Denies the maximize command without any pre-configured scope." + }, + { + "description": "Denies the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-minimize", + "markdownDescription": "Denies the minimize command without any pre-configured scope." + }, + { + "description": "Denies the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-monitor-from-point", + "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Denies the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-position", + "markdownDescription": "Denies the outer_position command without any pre-configured scope." + }, + { + "description": "Denies the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-size", + "markdownDescription": "Denies the outer_size command without any pre-configured scope." + }, + { + "description": "Denies the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-primary-monitor", + "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." + }, + { + "description": "Denies the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-request-user-attention", + "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." + }, + { + "description": "Denies the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scale-factor", + "markdownDescription": "Denies the scale_factor command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-bottom", + "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-top", + "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-background-color", + "markdownDescription": "Denies the set_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-count", + "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-label", + "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." + }, + { + "description": "Denies the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-closable", + "markdownDescription": "Denies the set_closable command without any pre-configured scope." + }, + { + "description": "Denies the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-content-protected", + "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-grab", + "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-icon", + "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-position", + "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-visible", + "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Denies the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-decorations", + "markdownDescription": "Denies the set_decorations command without any pre-configured scope." + }, + { + "description": "Denies the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-effects", + "markdownDescription": "Denies the set_effects command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focus", + "markdownDescription": "Denies the set_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focusable", + "markdownDescription": "Denies the set_focusable command without any pre-configured scope." + }, + { + "description": "Denies the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-fullscreen", + "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-ignore-cursor-events", + "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Denies the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-max-size", + "markdownDescription": "Denies the set_max_size command without any pre-configured scope." + }, + { + "description": "Denies the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-maximizable", + "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-min-size", + "markdownDescription": "Denies the set_min_size command without any pre-configured scope." + }, + { + "description": "Denies the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-minimizable", + "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-overlay-icon", + "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-position", + "markdownDescription": "Denies the set_position command without any pre-configured scope." + }, + { + "description": "Denies the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-progress-bar", + "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Denies the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-resizable", + "markdownDescription": "Denies the set_resizable command without any pre-configured scope." + }, + { + "description": "Denies the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-shadow", + "markdownDescription": "Denies the set_shadow command without any pre-configured scope." + }, + { + "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-simple-fullscreen", + "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size", + "markdownDescription": "Denies the set_size command without any pre-configured scope." + }, + { + "description": "Denies the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size-constraints", + "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Denies the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-skip-taskbar", + "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Denies the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-theme", + "markdownDescription": "Denies the set_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title-bar-style", + "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-visible-on-all-workspaces", + "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "Denies the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-dragging", + "markdownDescription": "Denies the start_dragging command without any pre-configured scope." + }, + { + "description": "Denies the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-resize-dragging", + "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Denies the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-theme", + "markdownDescription": "Denies the theme command without any pre-configured scope." + }, + { + "description": "Denies the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-title", + "markdownDescription": "Denies the title command without any pre-configured scope." + }, + { + "description": "Denies the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-toggle-maximize", + "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unmaximize", + "markdownDescription": "Denies the unmaximize command without any pre-configured scope." + }, + { + "description": "Denies the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unminimize", + "markdownDescription": "Denies the unminimize command without any pre-configured scope." + } + ] + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/src-tauri/gen/schemas/linux-schema.json b/src-tauri/gen/schemas/linux-schema.json new file mode 100644 index 0000000..260dbe0 --- /dev/null +++ b/src-tauri/gen/schemas/linux-schema.json @@ -0,0 +1,2244 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CapabilityFile", + "description": "Capability formats accepted in a capability file.", + "anyOf": [ + { + "description": "A single capability.", + "allOf": [ + { + "$ref": "#/definitions/Capability" + } + ] + }, + { + "description": "A list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + }, + { + "description": "A list of capabilities.", + "type": "object", + "required": [ + "capabilities" + ], + "properties": { + "capabilities": { + "description": "The list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + } + } + } + ], + "definitions": { + "Capability": { + "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", + "type": "object", + "required": [ + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", + "type": "string" + }, + "description": { + "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", + "default": "", + "type": "string" + }, + "remote": { + "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", + "anyOf": [ + { + "$ref": "#/definitions/CapabilityRemote" + }, + { + "type": "null" + } + ] + }, + "local": { + "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", + "default": true, + "type": "boolean" + }, + "windows": { + "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "webviews": { + "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "permissions": { + "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionEntry" + }, + "uniqueItems": true + }, + "platforms": { + "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "CapabilityRemote": { + "description": "Configuration for remote URLs that are associated with the capability.", + "type": "object", + "required": [ + "urls" + ], + "properties": { + "urls": { + "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionEntry": { + "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", + "anyOf": [ + { + "description": "Reference a permission or permission set by identifier.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + { + "description": "Reference a permission or permission set by identifier and extends its scope.", + "type": "object", + "allOf": [ + { + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + } + ], + "required": [ + "identifier" + ] + } + ] + }, + "Identifier": { + "description": "Permission identifier", + "oneOf": [ + { + "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", + "type": "string", + "const": "core:default", + "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`", + "type": "string", + "const": "core:app:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`" + }, + { + "description": "Enables the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-hide", + "markdownDescription": "Enables the app_hide command without any pre-configured scope." + }, + { + "description": "Enables the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-show", + "markdownDescription": "Enables the app_show command without any pre-configured scope." + }, + { + "description": "Enables the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-bundle-type", + "markdownDescription": "Enables the bundle_type command without any pre-configured scope." + }, + { + "description": "Enables the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-default-window-icon", + "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." + }, + { + "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-fetch-data-store-identifiers", + "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Enables the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-identifier", + "markdownDescription": "Enables the identifier command without any pre-configured scope." + }, + { + "description": "Enables the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-name", + "markdownDescription": "Enables the name command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-data-store", + "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." + }, + { + "description": "Enables the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-listener", + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." + }, + { + "description": "Enables the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-app-theme", + "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-dock-visibility", + "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Enables the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-tauri-version", + "markdownDescription": "Enables the tauri_version command without any pre-configured scope." + }, + { + "description": "Enables the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-version", + "markdownDescription": "Enables the version command without any pre-configured scope." + }, + { + "description": "Denies the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-hide", + "markdownDescription": "Denies the app_hide command without any pre-configured scope." + }, + { + "description": "Denies the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-show", + "markdownDescription": "Denies the app_show command without any pre-configured scope." + }, + { + "description": "Denies the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-bundle-type", + "markdownDescription": "Denies the bundle_type command without any pre-configured scope." + }, + { + "description": "Denies the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-default-window-icon", + "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." + }, + { + "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-fetch-data-store-identifiers", + "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Denies the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-identifier", + "markdownDescription": "Denies the identifier command without any pre-configured scope." + }, + { + "description": "Denies the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-name", + "markdownDescription": "Denies the name command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-data-store", + "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." + }, + { + "description": "Denies the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-listener", + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." + }, + { + "description": "Denies the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-app-theme", + "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-dock-visibility", + "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Denies the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-tauri-version", + "markdownDescription": "Denies the tauri_version command without any pre-configured scope." + }, + { + "description": "Denies the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-version", + "markdownDescription": "Denies the version command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", + "type": "string", + "const": "core:event:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" + }, + { + "description": "Enables the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit", + "markdownDescription": "Enables the emit command without any pre-configured scope." + }, + { + "description": "Enables the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit-to", + "markdownDescription": "Enables the emit_to command without any pre-configured scope." + }, + { + "description": "Enables the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-listen", + "markdownDescription": "Enables the listen command without any pre-configured scope." + }, + { + "description": "Enables the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-unlisten", + "markdownDescription": "Enables the unlisten command without any pre-configured scope." + }, + { + "description": "Denies the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit", + "markdownDescription": "Denies the emit command without any pre-configured scope." + }, + { + "description": "Denies the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit-to", + "markdownDescription": "Denies the emit_to command without any pre-configured scope." + }, + { + "description": "Denies the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-listen", + "markdownDescription": "Denies the listen command without any pre-configured scope." + }, + { + "description": "Denies the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-unlisten", + "markdownDescription": "Denies the unlisten command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", + "type": "string", + "const": "core:image:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" + }, + { + "description": "Enables the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-bytes", + "markdownDescription": "Enables the from_bytes command without any pre-configured scope." + }, + { + "description": "Enables the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-path", + "markdownDescription": "Enables the from_path command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-rgba", + "markdownDescription": "Enables the rgba command without any pre-configured scope." + }, + { + "description": "Enables the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-size", + "markdownDescription": "Enables the size command without any pre-configured scope." + }, + { + "description": "Denies the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-bytes", + "markdownDescription": "Denies the from_bytes command without any pre-configured scope." + }, + { + "description": "Denies the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-path", + "markdownDescription": "Denies the from_path command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-rgba", + "markdownDescription": "Denies the rgba command without any pre-configured scope." + }, + { + "description": "Denies the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-size", + "markdownDescription": "Denies the size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", + "type": "string", + "const": "core:menu:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" + }, + { + "description": "Enables the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-append", + "markdownDescription": "Enables the append command without any pre-configured scope." + }, + { + "description": "Enables the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-create-default", + "markdownDescription": "Enables the create_default command without any pre-configured scope." + }, + { + "description": "Enables the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-get", + "markdownDescription": "Enables the get command without any pre-configured scope." + }, + { + "description": "Enables the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-insert", + "markdownDescription": "Enables the insert command without any pre-configured scope." + }, + { + "description": "Enables the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-checked", + "markdownDescription": "Enables the is_checked command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-items", + "markdownDescription": "Enables the items command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-popup", + "markdownDescription": "Enables the popup command without any pre-configured scope." + }, + { + "description": "Enables the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-prepend", + "markdownDescription": "Enables the prepend command without any pre-configured scope." + }, + { + "description": "Enables the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove", + "markdownDescription": "Enables the remove command without any pre-configured scope." + }, + { + "description": "Enables the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove-at", + "markdownDescription": "Enables the remove_at command without any pre-configured scope." + }, + { + "description": "Enables the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-accelerator", + "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." + }, + { + "description": "Enables the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-app-menu", + "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-help-menu-for-nsapp", + "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-window-menu", + "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-windows-menu-for-nsapp", + "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-checked", + "markdownDescription": "Enables the set_checked command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-text", + "markdownDescription": "Enables the set_text command without any pre-configured scope." + }, + { + "description": "Enables the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-text", + "markdownDescription": "Enables the text command without any pre-configured scope." + }, + { + "description": "Denies the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-append", + "markdownDescription": "Denies the append command without any pre-configured scope." + }, + { + "description": "Denies the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-create-default", + "markdownDescription": "Denies the create_default command without any pre-configured scope." + }, + { + "description": "Denies the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-get", + "markdownDescription": "Denies the get command without any pre-configured scope." + }, + { + "description": "Denies the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-insert", + "markdownDescription": "Denies the insert command without any pre-configured scope." + }, + { + "description": "Denies the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-checked", + "markdownDescription": "Denies the is_checked command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-items", + "markdownDescription": "Denies the items command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-popup", + "markdownDescription": "Denies the popup command without any pre-configured scope." + }, + { + "description": "Denies the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-prepend", + "markdownDescription": "Denies the prepend command without any pre-configured scope." + }, + { + "description": "Denies the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove", + "markdownDescription": "Denies the remove command without any pre-configured scope." + }, + { + "description": "Denies the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove-at", + "markdownDescription": "Denies the remove_at command without any pre-configured scope." + }, + { + "description": "Denies the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-accelerator", + "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." + }, + { + "description": "Denies the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-app-menu", + "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-help-menu-for-nsapp", + "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-window-menu", + "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-windows-menu-for-nsapp", + "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-checked", + "markdownDescription": "Denies the set_checked command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-text", + "markdownDescription": "Denies the set_text command without any pre-configured scope." + }, + { + "description": "Denies the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-text", + "markdownDescription": "Denies the text command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", + "type": "string", + "const": "core:path:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" + }, + { + "description": "Enables the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-basename", + "markdownDescription": "Enables the basename command without any pre-configured scope." + }, + { + "description": "Enables the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-dirname", + "markdownDescription": "Enables the dirname command without any pre-configured scope." + }, + { + "description": "Enables the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-extname", + "markdownDescription": "Enables the extname command without any pre-configured scope." + }, + { + "description": "Enables the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-is-absolute", + "markdownDescription": "Enables the is_absolute command without any pre-configured scope." + }, + { + "description": "Enables the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-join", + "markdownDescription": "Enables the join command without any pre-configured scope." + }, + { + "description": "Enables the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-normalize", + "markdownDescription": "Enables the normalize command without any pre-configured scope." + }, + { + "description": "Enables the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve", + "markdownDescription": "Enables the resolve command without any pre-configured scope." + }, + { + "description": "Enables the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve-directory", + "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." + }, + { + "description": "Denies the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-basename", + "markdownDescription": "Denies the basename command without any pre-configured scope." + }, + { + "description": "Denies the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-dirname", + "markdownDescription": "Denies the dirname command without any pre-configured scope." + }, + { + "description": "Denies the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-extname", + "markdownDescription": "Denies the extname command without any pre-configured scope." + }, + { + "description": "Denies the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-is-absolute", + "markdownDescription": "Denies the is_absolute command without any pre-configured scope." + }, + { + "description": "Denies the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-join", + "markdownDescription": "Denies the join command without any pre-configured scope." + }, + { + "description": "Denies the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-normalize", + "markdownDescription": "Denies the normalize command without any pre-configured scope." + }, + { + "description": "Denies the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve", + "markdownDescription": "Denies the resolve command without any pre-configured scope." + }, + { + "description": "Denies the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve-directory", + "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", + "type": "string", + "const": "core:resources:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`", + "type": "string", + "const": "core:tray:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`" + }, + { + "description": "Enables the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-get-by-id", + "markdownDescription": "Enables the get_by_id command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-remove-by-id", + "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-as-template", + "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-menu", + "markdownDescription": "Enables the set_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-show-menu-on-left-click", + "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Enables the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-temp-dir-path", + "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-tooltip", + "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." + }, + { + "description": "Enables the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-visible", + "markdownDescription": "Enables the set_visible command without any pre-configured scope." + }, + { + "description": "Denies the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-get-by-id", + "markdownDescription": "Denies the get_by_id command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-remove-by-id", + "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-as-template", + "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-menu", + "markdownDescription": "Denies the set_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-show-menu-on-left-click", + "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Denies the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-temp-dir-path", + "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-tooltip", + "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." + }, + { + "description": "Denies the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-visible", + "markdownDescription": "Denies the set_visible command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", + "type": "string", + "const": "core:webview:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" + }, + { + "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-clear-all-browsing-data", + "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Enables the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview", + "markdownDescription": "Enables the create_webview command without any pre-configured scope." + }, + { + "description": "Enables the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview-window", + "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." + }, + { + "description": "Enables the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-get-all-webviews", + "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-internal-toggle-devtools", + "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Enables the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-print", + "markdownDescription": "Enables the print command without any pre-configured scope." + }, + { + "description": "Enables the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-reparent", + "markdownDescription": "Enables the reparent command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-auto-resize", + "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-background-color", + "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-focus", + "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-position", + "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-size", + "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-zoom", + "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Enables the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-close", + "markdownDescription": "Enables the webview_close command without any pre-configured scope." + }, + { + "description": "Enables the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-hide", + "markdownDescription": "Enables the webview_hide command without any pre-configured scope." + }, + { + "description": "Enables the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-position", + "markdownDescription": "Enables the webview_position command without any pre-configured scope." + }, + { + "description": "Enables the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-show", + "markdownDescription": "Enables the webview_show command without any pre-configured scope." + }, + { + "description": "Enables the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-size", + "markdownDescription": "Enables the webview_size command without any pre-configured scope." + }, + { + "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-clear-all-browsing-data", + "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Denies the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview", + "markdownDescription": "Denies the create_webview command without any pre-configured scope." + }, + { + "description": "Denies the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview-window", + "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." + }, + { + "description": "Denies the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-get-all-webviews", + "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-internal-toggle-devtools", + "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Denies the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-print", + "markdownDescription": "Denies the print command without any pre-configured scope." + }, + { + "description": "Denies the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-reparent", + "markdownDescription": "Denies the reparent command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-auto-resize", + "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-background-color", + "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-focus", + "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-position", + "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-size", + "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-zoom", + "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Denies the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-close", + "markdownDescription": "Denies the webview_close command without any pre-configured scope." + }, + { + "description": "Denies the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-hide", + "markdownDescription": "Denies the webview_hide command without any pre-configured scope." + }, + { + "description": "Denies the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-position", + "markdownDescription": "Denies the webview_position command without any pre-configured scope." + }, + { + "description": "Denies the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-show", + "markdownDescription": "Denies the webview_show command without any pre-configured scope." + }, + { + "description": "Denies the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-size", + "markdownDescription": "Denies the webview_size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`", + "type": "string", + "const": "core:window:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`" + }, + { + "description": "Enables the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-available-monitors", + "markdownDescription": "Enables the available_monitors command without any pre-configured scope." + }, + { + "description": "Enables the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-center", + "markdownDescription": "Enables the center command without any pre-configured scope." + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Enables the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-create", + "markdownDescription": "Enables the create command without any pre-configured scope." + }, + { + "description": "Enables the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-current-monitor", + "markdownDescription": "Enables the current_monitor command without any pre-configured scope." + }, + { + "description": "Enables the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-cursor-position", + "markdownDescription": "Enables the cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-destroy", + "markdownDescription": "Enables the destroy command without any pre-configured scope." + }, + { + "description": "Enables the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-get-all-windows", + "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." + }, + { + "description": "Enables the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-hide", + "markdownDescription": "Enables the hide command without any pre-configured scope." + }, + { + "description": "Enables the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-position", + "markdownDescription": "Enables the inner_position command without any pre-configured scope." + }, + { + "description": "Enables the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-size", + "markdownDescription": "Enables the inner_size command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-internal-toggle-maximize", + "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-always-on-top", + "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-closable", + "markdownDescription": "Enables the is_closable command without any pre-configured scope." + }, + { + "description": "Enables the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-decorated", + "markdownDescription": "Enables the is_decorated command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-focused", + "markdownDescription": "Enables the is_focused command without any pre-configured scope." + }, + { + "description": "Enables the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-fullscreen", + "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximizable", + "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximized", + "markdownDescription": "Enables the is_maximized command without any pre-configured scope." + }, + { + "description": "Enables the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimizable", + "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimized", + "markdownDescription": "Enables the is_minimized command without any pre-configured scope." + }, + { + "description": "Enables the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-resizable", + "markdownDescription": "Enables the is_resizable command without any pre-configured scope." + }, + { + "description": "Enables the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-visible", + "markdownDescription": "Enables the is_visible command without any pre-configured scope." + }, + { + "description": "Enables the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-maximize", + "markdownDescription": "Enables the maximize command without any pre-configured scope." + }, + { + "description": "Enables the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-minimize", + "markdownDescription": "Enables the minimize command without any pre-configured scope." + }, + { + "description": "Enables the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-monitor-from-point", + "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Enables the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-position", + "markdownDescription": "Enables the outer_position command without any pre-configured scope." + }, + { + "description": "Enables the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-size", + "markdownDescription": "Enables the outer_size command without any pre-configured scope." + }, + { + "description": "Enables the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-primary-monitor", + "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." + }, + { + "description": "Enables the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-request-user-attention", + "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." + }, + { + "description": "Enables the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scale-factor", + "markdownDescription": "Enables the scale_factor command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-bottom", + "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-top", + "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-background-color", + "markdownDescription": "Enables the set_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-count", + "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-label", + "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." + }, + { + "description": "Enables the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-closable", + "markdownDescription": "Enables the set_closable command without any pre-configured scope." + }, + { + "description": "Enables the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-content-protected", + "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-grab", + "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-icon", + "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-position", + "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-visible", + "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Enables the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-decorations", + "markdownDescription": "Enables the set_decorations command without any pre-configured scope." + }, + { + "description": "Enables the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-effects", + "markdownDescription": "Enables the set_effects command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focus", + "markdownDescription": "Enables the set_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focusable", + "markdownDescription": "Enables the set_focusable command without any pre-configured scope." + }, + { + "description": "Enables the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-fullscreen", + "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-ignore-cursor-events", + "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Enables the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-max-size", + "markdownDescription": "Enables the set_max_size command without any pre-configured scope." + }, + { + "description": "Enables the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-maximizable", + "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-min-size", + "markdownDescription": "Enables the set_min_size command without any pre-configured scope." + }, + { + "description": "Enables the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-minimizable", + "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-overlay-icon", + "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-position", + "markdownDescription": "Enables the set_position command without any pre-configured scope." + }, + { + "description": "Enables the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-progress-bar", + "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Enables the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-resizable", + "markdownDescription": "Enables the set_resizable command without any pre-configured scope." + }, + { + "description": "Enables the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-shadow", + "markdownDescription": "Enables the set_shadow command without any pre-configured scope." + }, + { + "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-simple-fullscreen", + "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size", + "markdownDescription": "Enables the set_size command without any pre-configured scope." + }, + { + "description": "Enables the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size-constraints", + "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Enables the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-skip-taskbar", + "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Enables the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-theme", + "markdownDescription": "Enables the set_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title-bar-style", + "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-visible-on-all-workspaces", + "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Enables the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-dragging", + "markdownDescription": "Enables the start_dragging command without any pre-configured scope." + }, + { + "description": "Enables the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-resize-dragging", + "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Enables the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-theme", + "markdownDescription": "Enables the theme command without any pre-configured scope." + }, + { + "description": "Enables the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-title", + "markdownDescription": "Enables the title command without any pre-configured scope." + }, + { + "description": "Enables the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-toggle-maximize", + "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unmaximize", + "markdownDescription": "Enables the unmaximize command without any pre-configured scope." + }, + { + "description": "Enables the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unminimize", + "markdownDescription": "Enables the unminimize command without any pre-configured scope." + }, + { + "description": "Denies the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-available-monitors", + "markdownDescription": "Denies the available_monitors command without any pre-configured scope." + }, + { + "description": "Denies the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-center", + "markdownDescription": "Denies the center command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Denies the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-create", + "markdownDescription": "Denies the create command without any pre-configured scope." + }, + { + "description": "Denies the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-current-monitor", + "markdownDescription": "Denies the current_monitor command without any pre-configured scope." + }, + { + "description": "Denies the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-cursor-position", + "markdownDescription": "Denies the cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-destroy", + "markdownDescription": "Denies the destroy command without any pre-configured scope." + }, + { + "description": "Denies the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-get-all-windows", + "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." + }, + { + "description": "Denies the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-hide", + "markdownDescription": "Denies the hide command without any pre-configured scope." + }, + { + "description": "Denies the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-position", + "markdownDescription": "Denies the inner_position command without any pre-configured scope." + }, + { + "description": "Denies the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-size", + "markdownDescription": "Denies the inner_size command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-internal-toggle-maximize", + "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-always-on-top", + "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-closable", + "markdownDescription": "Denies the is_closable command without any pre-configured scope." + }, + { + "description": "Denies the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-decorated", + "markdownDescription": "Denies the is_decorated command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-focused", + "markdownDescription": "Denies the is_focused command without any pre-configured scope." + }, + { + "description": "Denies the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-fullscreen", + "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximizable", + "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximized", + "markdownDescription": "Denies the is_maximized command without any pre-configured scope." + }, + { + "description": "Denies the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimizable", + "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimized", + "markdownDescription": "Denies the is_minimized command without any pre-configured scope." + }, + { + "description": "Denies the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-resizable", + "markdownDescription": "Denies the is_resizable command without any pre-configured scope." + }, + { + "description": "Denies the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-visible", + "markdownDescription": "Denies the is_visible command without any pre-configured scope." + }, + { + "description": "Denies the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-maximize", + "markdownDescription": "Denies the maximize command without any pre-configured scope." + }, + { + "description": "Denies the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-minimize", + "markdownDescription": "Denies the minimize command without any pre-configured scope." + }, + { + "description": "Denies the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-monitor-from-point", + "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Denies the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-position", + "markdownDescription": "Denies the outer_position command without any pre-configured scope." + }, + { + "description": "Denies the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-size", + "markdownDescription": "Denies the outer_size command without any pre-configured scope." + }, + { + "description": "Denies the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-primary-monitor", + "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." + }, + { + "description": "Denies the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-request-user-attention", + "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." + }, + { + "description": "Denies the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scale-factor", + "markdownDescription": "Denies the scale_factor command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-bottom", + "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-top", + "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-background-color", + "markdownDescription": "Denies the set_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-count", + "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-label", + "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." + }, + { + "description": "Denies the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-closable", + "markdownDescription": "Denies the set_closable command without any pre-configured scope." + }, + { + "description": "Denies the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-content-protected", + "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-grab", + "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-icon", + "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-position", + "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-visible", + "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Denies the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-decorations", + "markdownDescription": "Denies the set_decorations command without any pre-configured scope." + }, + { + "description": "Denies the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-effects", + "markdownDescription": "Denies the set_effects command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focus", + "markdownDescription": "Denies the set_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focusable", + "markdownDescription": "Denies the set_focusable command without any pre-configured scope." + }, + { + "description": "Denies the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-fullscreen", + "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-ignore-cursor-events", + "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Denies the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-max-size", + "markdownDescription": "Denies the set_max_size command without any pre-configured scope." + }, + { + "description": "Denies the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-maximizable", + "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-min-size", + "markdownDescription": "Denies the set_min_size command without any pre-configured scope." + }, + { + "description": "Denies the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-minimizable", + "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-overlay-icon", + "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-position", + "markdownDescription": "Denies the set_position command without any pre-configured scope." + }, + { + "description": "Denies the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-progress-bar", + "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Denies the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-resizable", + "markdownDescription": "Denies the set_resizable command without any pre-configured scope." + }, + { + "description": "Denies the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-shadow", + "markdownDescription": "Denies the set_shadow command without any pre-configured scope." + }, + { + "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-simple-fullscreen", + "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size", + "markdownDescription": "Denies the set_size command without any pre-configured scope." + }, + { + "description": "Denies the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size-constraints", + "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Denies the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-skip-taskbar", + "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Denies the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-theme", + "markdownDescription": "Denies the set_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title-bar-style", + "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-visible-on-all-workspaces", + "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "Denies the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-dragging", + "markdownDescription": "Denies the start_dragging command without any pre-configured scope." + }, + { + "description": "Denies the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-resize-dragging", + "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Denies the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-theme", + "markdownDescription": "Denies the theme command without any pre-configured scope." + }, + { + "description": "Denies the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-title", + "markdownDescription": "Denies the title command without any pre-configured scope." + }, + { + "description": "Denies the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-toggle-maximize", + "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unmaximize", + "markdownDescription": "Denies the unmaximize command without any pre-configured scope." + }, + { + "description": "Denies the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unminimize", + "markdownDescription": "Denies the unminimize command without any pre-configured scope." + } + ] + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100644 index 0000000..7b252fb Binary files /dev/null and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/pdfjs/package.json b/src-tauri/pdfjs/package.json new file mode 100644 index 0000000..a27d6f8 --- /dev/null +++ b/src-tauri/pdfjs/package.json @@ -0,0 +1,8 @@ +{ + "name": "brittle-pdfjs", + "private": true, + "description": "Pre-built pdfjs-dist for the Brittle PDF viewer.", + "dependencies": { + "pdfjs-dist": "3.11.174" + } +} diff --git a/src-tauri/src/commands/annotation.rs b/src-tauri/src/commands/annotation.rs new file mode 100644 index 0000000..d091c57 --- /dev/null +++ b/src-tauri/src/commands/annotation.rs @@ -0,0 +1,41 @@ +//! Tauri commands for PDF annotation CRUD. + +use crate::state::AppState; +use brittle_core::{Annotation, AnnotationId, AnnotationType, ReferenceId}; +use tauri::State; + +#[tauri::command] +pub fn create_annotation( + state: State, + reference_id: ReferenceId, + page: u32, + annotation_type: AnnotationType, + content: Option, +) -> Result { + state.with_repo(|b| b.create_annotation(reference_id, page, annotation_type, content)) +} + +#[tauri::command] +pub fn get_annotations( + state: State, + reference_id: ReferenceId, +) -> Result, String> { + state.with_repo_read(|b| b.get_annotations(reference_id)) +} + +#[tauri::command] +pub fn update_annotation( + state: State, + annotation: Annotation, +) -> Result { + state.with_repo(|b| b.update_annotation(annotation)) +} + +#[tauri::command] +pub fn delete_annotation( + state: State, + reference_id: ReferenceId, + annotation_id: AnnotationId, +) -> Result<(), String> { + state.with_repo(|b| b.delete_annotation(reference_id, annotation_id)) +} diff --git a/src-tauri/src/commands/bibtex.rs b/src-tauri/src/commands/bibtex.rs new file mode 100644 index 0000000..92d296a --- /dev/null +++ b/src-tauri/src/commands/bibtex.rs @@ -0,0 +1,44 @@ +//! Tauri commands for BibTeX export. + +use crate::state::AppState; +use brittle_core::{LibraryId, ReferenceId}; +use serde::Serialize; +use tauri::State; + +/// Result of a BibTeX export: the formatted string plus any non-fatal errors. +#[derive(Serialize)] +pub struct BibtexExportResult { + pub bibtex: String, + /// Warnings for references that were skipped due to missing required fields. + pub errors: Vec, +} + +/// Export a list of references as BibTeX. +#[tauri::command] +pub fn export_bibtex( + state: State, + reference_ids: Vec, +) -> Result { + state.with_repo_read(|b| { + let (bibtex, errors) = b.export_bibtex(&reference_ids)?; + Ok(BibtexExportResult { + bibtex, + errors: errors.iter().map(|e| e.to_string()).collect(), + }) + }) +} + +/// Export all references in a library as BibTeX. +#[tauri::command] +pub fn export_library_bibtex( + state: State, + library_id: LibraryId, +) -> Result { + state.with_repo_read(|b| { + let (bibtex, errors) = b.export_library_bibtex(library_id)?; + Ok(BibtexExportResult { + bibtex, + errors: errors.iter().map(|e| e.to_string()).collect(), + }) + }) +} diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs new file mode 100644 index 0000000..7800dd8 --- /dev/null +++ b/src-tauri/src/commands/config.rs @@ -0,0 +1,73 @@ +//! Tauri commands for reading and writing configuration. + +use std::path::Path; + +use crate::config::{GlobalConfig, ProjectConfig}; + +/// Load the global config from `~/.config/brittle/config.toml`. +/// Returns the default config if the file does not yet exist. +#[tauri::command] +pub fn load_global_config() -> Result { + GlobalConfig::load().map_err(|e| e.to_string()) +} + +/// Persist the global config to `~/.config/brittle/config.toml`. +#[tauri::command] +pub fn save_global_config(config: GlobalConfig) -> Result<(), String> { + config.save().map_err(|e| e.to_string()) +} + +/// Load the per-project config from `{repo_path}/.brittle/config.toml`. +/// Returns the default config if the file does not yet exist. +#[tauri::command] +pub fn load_project_config(repo_path: String) -> Result { + ProjectConfig::load(Path::new(&repo_path)).map_err(|e| e.to_string()) +} + +/// Persist the per-project config to `{repo_path}/.brittle/config.toml`. +#[tauri::command] +pub fn save_project_config(repo_path: String, config: ProjectConfig) -> Result<(), String> { + config + .save(Path::new(&repo_path)) + .map_err(|e| e.to_string()) +} + +/// Return the current theme name (`"dark"` or `"light"`) from the global config. +#[tauri::command] +pub fn get_theme() -> Result { + use crate::config::Theme; + GlobalConfig::load() + .map(|c| match c.appearance.theme { + Theme::Dark => "dark".to_string(), + Theme::Light => "light".to_string(), + }) + .map_err(|e| e.to_string()) +} + +/// Persist a new theme choice to the global config. +/// +/// `theme` must be `"dark"` or `"light"`. +#[tauri::command] +pub fn set_theme(theme: String) -> Result<(), String> { + use crate::config::Theme; + let parsed = match theme.as_str() { + "dark" => Theme::Dark, + "light" => Theme::Light, + other => return Err(format!("unknown theme '{other}'")), + }; + let mut config = GlobalConfig::load().map_err(|e| e.to_string())?; + config.appearance.theme = parsed; + config.save().map_err(|e| e.to_string()) +} + +/// Return the user's keybinding overrides from the global config. +/// +/// Map keys are action names in snake_case (e.g. `"tab_next"`); values are +/// key-sequence strings (e.g. `""`). Returns an empty map if no +/// config file exists or the `[keybindings]` section is absent. +#[tauri::command] +pub fn get_keybindings() -> Result, String> { + GlobalConfig::load() + .map(|c| c.keybindings.0) + .map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/commands/library.rs b/src-tauri/src/commands/library.rs new file mode 100644 index 0000000..fc9522b --- /dev/null +++ b/src-tauri/src/commands/library.rs @@ -0,0 +1,97 @@ +//! Tauri commands for library CRUD, hierarchy, and membership. + +use crate::state::AppState; +use brittle_core::{Library, LibraryId, ReferenceId}; +use tauri::State; + +#[tauri::command] +pub fn create_library( + state: State, + name: String, + parent_id: Option, +) -> Result { + state.with_repo(|b| b.create_library(name, parent_id)) +} + +#[tauri::command] +pub fn get_library(state: State, id: LibraryId) -> Result { + state.with_repo_read(|b| b.get_library(id)) +} + +#[tauri::command] +pub fn rename_library( + state: State, + id: LibraryId, + new_name: String, +) -> Result { + state.with_repo(|b| b.rename_library(id, new_name)) +} + +#[tauri::command] +pub fn move_library( + state: State, + id: LibraryId, + new_parent: Option, +) -> Result { + state.with_repo(|b| b.move_library(id, new_parent)) +} + +/// Delete a library. Fails if it has child libraries. +#[tauri::command] +pub fn delete_library(state: State, id: LibraryId) -> Result<(), String> { + state.with_repo(|b| b.delete_library(id)) +} + +/// Delete a library and all its descendants (recursive). +#[tauri::command] +pub fn force_delete_library(state: State, id: LibraryId) -> Result<(), String> { + state.with_repo(|b| b.force_delete_library(id)) +} + +#[tauri::command] +pub fn list_root_libraries(state: State) -> Result, String> { + state.with_repo_read(|b| b.list_root_libraries()) +} + +#[tauri::command] +pub fn list_child_libraries( + state: State, + parent_id: LibraryId, +) -> Result, String> { + state.with_repo_read(|b| b.list_child_libraries(parent_id)) +} + +/// Return the ancestor chain of a library, ordered root → direct parent. +#[tauri::command] +pub fn get_library_ancestors( + state: State, + id: LibraryId, +) -> Result, String> { + state.with_repo_read(|b| b.get_library_ancestors(id)) +} + +#[tauri::command] +pub fn add_to_library( + state: State, + library_id: LibraryId, + reference_id: ReferenceId, +) -> Result<(), String> { + state.with_repo(|b| b.add_to_library(library_id, reference_id)) +} + +#[tauri::command] +pub fn remove_from_library( + state: State, + library_id: LibraryId, + reference_id: ReferenceId, +) -> Result<(), String> { + state.with_repo(|b| b.remove_from_library(library_id, reference_id)) +} + +#[tauri::command] +pub fn list_reference_libraries( + state: State, + reference_id: ReferenceId, +) -> Result, String> { + state.with_repo_read(|b| b.list_reference_libraries(reference_id)) +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs new file mode 100644 index 0000000..a735e18 --- /dev/null +++ b/src-tauri/src/commands/mod.rs @@ -0,0 +1,9 @@ +pub mod annotation; +pub mod bibtex; +pub mod config; +pub mod library; +pub mod pdf; +pub mod reference; +pub mod repository; +pub mod snapshot; +pub mod window; diff --git a/src-tauri/src/commands/pdf.rs b/src-tauri/src/commands/pdf.rs new file mode 100644 index 0000000..e631606 --- /dev/null +++ b/src-tauri/src/commands/pdf.rs @@ -0,0 +1,31 @@ +//! Tauri commands for PDF attachment and retrieval. + +use crate::state::AppState; +use brittle_core::{PdfAttachment, ReferenceId}; +use std::path::PathBuf; +use tauri::State; + +/// Attach a PDF file to a reference by copying it into the repository. +/// +/// `source_path` is the absolute path to the file to copy. +/// Returns the stored attachment metadata. +#[tauri::command] +pub fn attach_pdf( + state: State, + reference_id: ReferenceId, + source_path: String, +) -> Result { + let path = PathBuf::from(&source_path); + state.with_repo(|b| b.attach_pdf(reference_id, &path)) +} + +/// Return the absolute filesystem path to the PDF attached to a reference. +/// +/// Returns an error if no PDF is attached. +#[tauri::command] +pub fn get_pdf_path(state: State, reference_id: ReferenceId) -> Result { + state.with_repo_read(|b| { + b.get_pdf_path(reference_id) + .map(|p| p.to_string_lossy().into_owned()) + }) +} diff --git a/src-tauri/src/commands/reference.rs b/src-tauri/src/commands/reference.rs new file mode 100644 index 0000000..42c24bc --- /dev/null +++ b/src-tauri/src/commands/reference.rs @@ -0,0 +1,87 @@ +//! Tauri commands for reference CRUD and search. + +use crate::state::AppState; +use brittle_core::{EntryType, LibraryId, Reference, ReferenceId, ReferenceSummary}; +use tauri::State; + +#[tauri::command] +pub fn create_reference( + state: State, + cite_key: String, + entry_type: EntryType, +) -> Result { + state.with_repo(|b| b.create_reference(cite_key, entry_type)) +} + +#[tauri::command] +pub fn get_reference(state: State, id: ReferenceId) -> Result { + state.with_repo_read(|b| b.get_reference(id)) +} + +#[tauri::command] +pub fn update_reference(state: State, reference: Reference) -> Result { + state.with_repo(|b| b.update_reference(reference)) +} + +#[tauri::command] +pub fn delete_reference(state: State, id: ReferenceId) -> Result<(), String> { + state.with_repo(|b| b.delete_reference(id)) +} + +#[tauri::command] +pub fn list_references(state: State) -> Result, String> { + state.with_repo_read(|b| b.list_references()) +} + +#[tauri::command] +pub fn set_field( + state: State, + id: ReferenceId, + field: String, + value: String, +) -> Result<(), String> { + state.with_repo(|b| b.set_field(id, &field, value)) +} + +#[tauri::command] +pub fn remove_field(state: State, id: ReferenceId, field: String) -> Result<(), String> { + state.with_repo(|b| b.remove_field(id, &field)) +} + +#[tauri::command] +pub fn search_references( + state: State, + query: String, +) -> Result, String> { + state.with_repo_read(|b| b.search_references(&query)) +} + +#[tauri::command] +pub fn search_library_references( + state: State, + library_id: LibraryId, + query: String, +) -> Result, String> { + state.with_repo_read(|b| b.search_library_references(library_id, &query)) +} + +#[tauri::command] +pub fn list_library_references( + state: State, + library_id: LibraryId, +) -> Result, String> { + state.with_repo_read(|b| b.list_library_references(library_id)) +} + +#[tauri::command] +pub fn list_library_references_recursive( + state: State, + library_id: LibraryId, +) -> Result, String> { + let result = state.with_repo_read(|b| b.list_library_references_recursive(library_id)); + match &result { + Ok(refs) => eprintln!("[brittle] list_library_references_recursive({library_id}): {} refs", refs.len()), + Err(e) => eprintln!("[brittle] list_library_references_recursive({library_id}): ERROR: {e}"), + } + result +} diff --git a/src-tauri/src/commands/repository.rs b/src-tauri/src/commands/repository.rs new file mode 100644 index 0000000..df45637 --- /dev/null +++ b/src-tauri/src/commands/repository.rs @@ -0,0 +1,60 @@ +//! Tauri commands for repository lifecycle (create, open, close). + +use crate::state::AppState; +use brittle_core::Brittle; +use std::path::PathBuf; +use tauri::State; + +fn expand_tilde(path: &str) -> PathBuf { + if let Some(rest) = path.strip_prefix("~/") { + if let Ok(home) = std::env::var("HOME") { + return PathBuf::from(home).join(rest); + } + } + if path == "~" { + if let Ok(home) = std::env::var("HOME") { + return PathBuf::from(home); + } + } + PathBuf::from(path) +} + +/// Create a new Brittle repository at `path` and open it. +#[tauri::command] +pub fn create_repository(state: State, path: String) -> Result<(), String> { + let path = expand_tilde(&path); + let brittle = Brittle::create(&path).map_err(|e| e.to_string())?; + *state + .brittle + .lock() + .map_err(|_| "lock poisoned".to_string())? = Some(brittle); + Ok(()) +} + +/// Open an existing Brittle repository at `path`. +#[tauri::command] +pub fn open_repository(state: State, path: String) -> Result<(), String> { + let path = expand_tilde(&path); + let brittle = Brittle::open(&path).map_err(|e| e.to_string())?; + *state + .brittle + .lock() + .map_err(|_| "lock poisoned".to_string())? = Some(brittle); + Ok(()) +} + +/// Close the currently open repository. No-op if none is open. +#[tauri::command] +pub fn close_repository(state: State) -> Result<(), String> { + *state + .brittle + .lock() + .map_err(|_| "lock poisoned".to_string())? = None; + Ok(()) +} + +/// Return the filesystem path of the currently open repository. +#[tauri::command] +pub fn repository_root(state: State) -> Result { + state.with_repo_read(|b| Ok(b.repository_root().to_string_lossy().into_owned())) +} diff --git a/src-tauri/src/commands/snapshot.rs b/src-tauri/src/commands/snapshot.rs new file mode 100644 index 0000000..7bdbec6 --- /dev/null +++ b/src-tauri/src/commands/snapshot.rs @@ -0,0 +1,33 @@ +//! Tauri commands for snapshotting (git-backed history). + +use crate::state::AppState; +use brittle_core::Snapshot; +use tauri::State; + +#[tauri::command] +pub fn create_snapshot(state: State, message: String) -> Result { + state.with_repo(|b| b.create_snapshot(&message)) +} + +#[tauri::command] +pub fn list_snapshots(state: State) -> Result, String> { + state.with_repo_read(|b| b.list_snapshots()) +} + +/// Restore to a named snapshot. Fails if there are uncommitted changes. +/// Use `discard_changes` first if needed. +#[tauri::command] +pub fn restore_snapshot(state: State, snapshot_id: String) -> Result<(), String> { + state.with_repo(|b| b.restore_snapshot(&snapshot_id)) +} + +#[tauri::command] +pub fn has_uncommitted_changes(state: State) -> Result { + state.with_repo_read(|b| b.has_uncommitted_changes()) +} + +/// Discard all uncommitted changes, reverting to the last snapshot. +#[tauri::command] +pub fn discard_changes(state: State) -> Result<(), String> { + state.with_repo(|b| b.discard_changes()) +} diff --git a/src-tauri/src/commands/window.rs b/src-tauri/src/commands/window.rs new file mode 100644 index 0000000..898a1f5 --- /dev/null +++ b/src-tauri/src/commands/window.rs @@ -0,0 +1,54 @@ +//! Tauri commands for managing PDF viewer windows. + +use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder}; + +/// Open a PDF viewer window for the given reference ID. +/// +/// If a window for this reference is already open it is focused instead of +/// creating a duplicate. +/// +/// The window loads `brittle://app/viewer?ref_id=`. +#[tauri::command] +pub fn open_pdf_window(app: AppHandle, ref_id: String) -> Result<(), String> { + let label = format!("pdf-{}", ref_id); + + // If already open, just focus it. + if let Some(win) = app.get_webview_window(&label) { + win.set_focus().map_err(|e| e.to_string())?; + return Ok(()); + } + + let url_str = format!( + "brittle://app/viewer?ref_id={}", + urlencoding::encode(&ref_id) + ); + let url = url_str.parse::().map_err(|e| e.to_string())?; + + WebviewWindowBuilder::new(&app, &label, WebviewUrl::External(url)) + .title("PDF Viewer — Brittle") + .inner_size(900.0, 750.0) + .min_inner_size(600.0, 400.0) + .build() + .map_err(|e| e.to_string())?; + + Ok(()) +} + +/// Close the PDF viewer window for the given reference ID, if open. +#[tauri::command] +pub fn close_pdf_window(app: AppHandle, ref_id: String) -> Result<(), String> { + let label = format!("pdf-{}", ref_id); + if let Some(win) = app.get_webview_window(&label) { + win.close().map_err(|e| e.to_string())?; + } + Ok(()) +} + +/// Return the labels of all currently open PDF viewer windows. +#[tauri::command] +pub fn list_pdf_windows(app: AppHandle) -> Vec { + app.webview_windows() + .into_keys() + .filter(|label| label.starts_with("pdf-")) + .collect() +} diff --git a/src-tauri/src/config/global.rs b/src-tauri/src/config/global.rs new file mode 100644 index 0000000..26877c7 --- /dev/null +++ b/src-tauri/src/config/global.rs @@ -0,0 +1,183 @@ +//! Global (user-wide) configuration. + +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +use serde::{Deserialize, Serialize}; + +use super::{AppearanceConfig, ConfigError, KeybindingsConfig, LayoutConfig}; + +/// Record of recently opened repositories, stored in the global config. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[serde(default)] +pub struct ProjectsState { + pub recent: Vec, + pub last_opened: Option, +} + +/// User-wide configuration, stored at `~/.config/brittle/config.toml`. +/// +/// All fields are optional in the file; missing fields fall back to their +/// `Default` implementations. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[serde(default)] +pub struct GlobalConfig { + pub appearance: AppearanceConfig, + pub layout: LayoutConfig, + pub projects: ProjectsState, + /// Flat map of action name → key combo string for user-defined overrides. + #[serde(with = "keybindings_map")] + pub keybindings: KeybindingsConfig, +} + +impl GlobalConfig { + /// Load from the standard platform config directory. + /// + /// Returns `Default` if the file does not yet exist. + pub fn load() -> Result { + Self::load_from(&global_config_path()?) + } + + /// Load from an explicit path. + /// + /// Returns `Default` if the file does not exist. + pub fn load_from(path: &Path) -> Result { + if !path.exists() { + return Ok(Self::default()); + } + let content = std::fs::read_to_string(path)?; + Ok(toml::from_str(&content)?) + } + + /// Save to the standard platform config directory, + /// creating parent directories as needed. + pub fn save(&self) -> Result<(), ConfigError> { + self.save_to(&global_config_path()?) + } + + /// Save to an explicit path, creating parent directories as needed. + pub fn save_to(&self, path: &Path) -> Result<(), ConfigError> { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(path, toml::to_string_pretty(self)?)?; + Ok(()) + } +} + +fn global_config_path() -> Result { + dirs::config_dir() + .ok_or(ConfigError::NoConfigDir) + .map(|d| d.join("brittle").join("config.toml")) +} + +/// Custom serde module so `KeybindingsConfig` round-trips as a flat TOML table. +/// +/// Stored in the file as: +/// ```toml +/// [keybindings] +/// focus_left = "H" +/// tab_next = "gt" +/// ``` +mod keybindings_map { + use super::*; + use serde::{Deserializer, Serializer}; + + pub fn serialize(kc: &KeybindingsConfig, s: S) -> Result { + kc.0.serialize(s) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result { + HashMap::::deserialize(d).map(KeybindingsConfig) + } +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::Theme; + use tempfile::TempDir; + + #[test] + fn global_config_defaults() { + let cfg = GlobalConfig::default(); + assert_eq!(cfg.appearance.theme, Theme::Dark); + assert_eq!(cfg.appearance.font_size, 14); + assert!(cfg.projects.recent.is_empty()); + assert!(cfg.keybindings.0.is_empty()); + } + + #[test] + fn global_config_round_trips() { + let mut cfg = GlobalConfig::default(); + cfg.appearance.theme = Theme::Light; + cfg.appearance.font_size = 16; + cfg.keybindings + .0 + .insert("focus_left".to_string(), "C-h".to_string()); + + let s = toml::to_string_pretty(&cfg).unwrap(); + let parsed: GlobalConfig = toml::from_str(&s).unwrap(); + assert_eq!(parsed, cfg); + } + + #[test] + fn empty_toml_uses_all_defaults() { + let cfg: GlobalConfig = toml::from_str("").unwrap(); + assert_eq!(cfg.appearance.font_size, 14); + assert!((cfg.layout.left_pane_fraction - 0.20).abs() < f32::EPSILON); + } + + #[test] + fn partial_toml_uses_defaults_for_missing_sections() { + let toml = "[appearance]\ntheme = \"light\"\n"; + let cfg: GlobalConfig = toml::from_str(toml).unwrap(); + assert_eq!(cfg.appearance.theme, Theme::Light); + // layout not specified — should be default + assert!((cfg.layout.left_pane_fraction - 0.20).abs() < f32::EPSILON); + } + + #[test] + fn load_from_nonexistent_path_returns_default() { + let tmp = TempDir::new().unwrap(); + let path = tmp.path().join("does_not_exist.toml"); + let cfg = GlobalConfig::load_from(&path).unwrap(); + assert_eq!(cfg, GlobalConfig::default()); + } + + #[test] + fn save_to_and_load_from_round_trip() { + let tmp = TempDir::new().unwrap(); + let path = tmp.path().join("config.toml"); + + let mut original = GlobalConfig::default(); + original.appearance.theme = Theme::Light; + original.appearance.font_size = 18; + original.save_to(&path).unwrap(); + + let loaded = GlobalConfig::load_from(&path).unwrap(); + assert_eq!(loaded, original); + } + + #[test] + fn save_to_creates_parent_directories() { + let tmp = TempDir::new().unwrap(); + let path = tmp.path().join("nested").join("dirs").join("config.toml"); + GlobalConfig::default().save_to(&path).unwrap(); + assert!(path.exists()); + } + + #[test] + fn keybinding_overrides_round_trip() { + let mut cfg = GlobalConfig::default(); + cfg.keybindings + .0 + .insert("tab_next".to_string(), "C-Right".to_string()); + + let s = toml::to_string_pretty(&cfg).unwrap(); + let parsed: GlobalConfig = toml::from_str(&s).unwrap(); + assert_eq!(parsed.keybindings.0["tab_next"], "C-Right"); + } +} diff --git a/src-tauri/src/config/mod.rs b/src-tauri/src/config/mod.rs new file mode 100644 index 0000000..38a1c69 --- /dev/null +++ b/src-tauri/src/config/mod.rs @@ -0,0 +1,279 @@ +//! Configuration types for Brittle. +//! +//! Two levels of config exist: +//! - [`GlobalConfig`] — stored at `~/.config/brittle/config.toml`; applies to all projects. +//! - [`ProjectConfig`] — stored at `{repo}/.brittle/config.toml`; overrides globals per project. +//! +//! Use [`MergedConfig::merge`] to produce the effective config the app should use. + +mod global; +mod project; + +pub use global::GlobalConfig; +pub use project::ProjectConfig; + +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +// ── Shared types ────────────────────────────────────────────────────────────── + +/// Application colour theme. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "lowercase")] +pub enum Theme { + #[default] + Dark, + Light, +} + +/// Appearance settings (font size, theme). +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(default)] +pub struct AppearanceConfig { + pub theme: Theme, + pub font_size: u32, +} + +impl Default for AppearanceConfig { + fn default() -> Self { + Self { + theme: Theme::Dark, + font_size: 14, + } + } +} + +/// Partial appearance override from a project config. +/// Only `Some` fields replace the global value. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[serde(default)] +pub struct AppearanceOverride { + pub theme: Option, + pub font_size: Option, +} + +/// Pane layout proportions (fractions of the window width, 0..1). +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(default)] +pub struct LayoutConfig { + pub left_pane_fraction: f32, + pub right_pane_fraction: f32, +} + +impl Default for LayoutConfig { + fn default() -> Self { + Self { + left_pane_fraction: 0.20, + right_pane_fraction: 0.35, + } + } +} + +/// Keybinding overrides — maps action name to key combo string. +/// +/// Only actions the user wants to rebind need an entry here; everything else +/// falls back to the built-in defaults defined in `keymap`. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub struct KeybindingsConfig(pub HashMap); + +// ── Merged config ───────────────────────────────────────────────────────────── + +/// The effective config the application uses at runtime, produced by merging +/// global defaults with per-project overrides. +#[derive(Debug, Clone)] +#[allow(dead_code)] // consumed in Phase 3 when AppState is wired up +pub struct MergedConfig { + pub appearance: AppearanceConfig, + pub layout: LayoutConfig, + pub keybindings: KeybindingsConfig, + /// Reference IDs of tabs that should be restored on launch. + pub open_tabs: Vec, +} + +impl MergedConfig { + #[allow(dead_code)] // consumed in Phase 3 when AppState is wired up + /// Merge `global` with an optional `project` override. + /// + /// Project values take precedence for appearance; layout and keybindings + /// are always taken from the global config (per-project overrides for those + /// are intentionally not supported — it would be confusing). + pub fn merge(global: &GlobalConfig, project: Option<&ProjectConfig>) -> Self { + let appearance = match project.and_then(|p| p.appearance.as_ref()) { + Some(ov) => AppearanceConfig { + theme: ov + .theme + .clone() + .unwrap_or_else(|| global.appearance.theme.clone()), + font_size: ov.font_size.unwrap_or(global.appearance.font_size), + }, + None => global.appearance.clone(), + }; + + Self { + appearance, + layout: global.layout.clone(), + keybindings: global.keybindings.clone(), + open_tabs: project + .map(|p| p.session.open_tabs.clone()) + .unwrap_or_default(), + } + } +} + +// ── Error type ──────────────────────────────────────────────────────────────── + +#[derive(Debug, thiserror::Error)] +pub enum ConfigError { + #[error("no config directory found on this platform")] + NoConfigDir, + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + #[error("TOML parse error: {0}")] + Parse(#[from] toml::de::Error), + #[error("TOML serialize error: {0}")] + Serialize(#[from] toml::ser::Error), +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::project::SessionConfig; + + // ── AppearanceConfig ────────────────────────────────────────────────────── + + #[test] + fn appearance_defaults_are_dark_14pt() { + let a = AppearanceConfig::default(); + assert_eq!(a.theme, Theme::Dark); + assert_eq!(a.font_size, 14); + } + + #[test] + fn appearance_round_trips() { + let original = AppearanceConfig { + theme: Theme::Light, + font_size: 16, + }; + let s = toml::to_string_pretty(&original).unwrap(); + let parsed: AppearanceConfig = toml::from_str(&s).unwrap(); + assert_eq!(parsed, original); + } + + #[test] + fn appearance_missing_fields_use_defaults() { + // Only theme specified; font_size should fall back to 14. + let parsed: AppearanceConfig = toml::from_str("theme = \"light\"").unwrap(); + assert_eq!(parsed.theme, Theme::Light); + assert_eq!(parsed.font_size, 14); + } + + // ── LayoutConfig ───────────────────────────────────────────────────────── + + #[test] + fn layout_defaults() { + let l = LayoutConfig::default(); + assert!((l.left_pane_fraction - 0.20).abs() < f32::EPSILON); + assert!((l.right_pane_fraction - 0.35).abs() < f32::EPSILON); + } + + #[test] + fn layout_round_trips() { + let original = LayoutConfig { + left_pane_fraction: 0.25, + right_pane_fraction: 0.40, + }; + let s = toml::to_string_pretty(&original).unwrap(); + let parsed: LayoutConfig = toml::from_str(&s).unwrap(); + assert_eq!(parsed, original); + } + + // ── MergedConfig ───────────────────────────────────────────────────────── + + #[test] + fn merge_without_project_uses_globals() { + let global = GlobalConfig { + appearance: AppearanceConfig { + theme: Theme::Light, + font_size: 16, + }, + ..Default::default() + }; + let merged = MergedConfig::merge(&global, None); + assert_eq!(merged.appearance.theme, Theme::Light); + assert_eq!(merged.appearance.font_size, 16); + assert!(merged.open_tabs.is_empty()); + } + + #[test] + fn merge_project_overrides_theme() { + let global = GlobalConfig { + appearance: AppearanceConfig { + theme: Theme::Dark, + font_size: 14, + }, + ..Default::default() + }; + let project = ProjectConfig { + appearance: Some(AppearanceOverride { + theme: Some(Theme::Light), + font_size: None, + }), + ..Default::default() + }; + let merged = MergedConfig::merge(&global, Some(&project)); + assert_eq!(merged.appearance.theme, Theme::Light); + // font_size not overridden — inherits from global + assert_eq!(merged.appearance.font_size, 14); + } + + #[test] + fn merge_project_overrides_font_size_only() { + let global = GlobalConfig { + appearance: AppearanceConfig { + theme: Theme::Dark, + font_size: 14, + }, + ..Default::default() + }; + let project = ProjectConfig { + appearance: Some(AppearanceOverride { + theme: None, + font_size: Some(18), + }), + ..Default::default() + }; + let merged = MergedConfig::merge(&global, Some(&project)); + assert_eq!(merged.appearance.theme, Theme::Dark); + assert_eq!(merged.appearance.font_size, 18); + } + + #[test] + fn merge_project_open_tabs_are_included() { + let global = GlobalConfig::default(); + let project = ProjectConfig { + session: SessionConfig { + open_tabs: vec!["tab-a".to_string(), "tab-b".to_string()], + }, + ..Default::default() + }; + let merged = MergedConfig::merge(&global, Some(&project)); + assert_eq!(merged.open_tabs, ["tab-a", "tab-b"]); + } + + #[test] + fn merge_layout_always_from_global() { + let global = GlobalConfig { + layout: LayoutConfig { + left_pane_fraction: 0.30, + right_pane_fraction: 0.40, + }, + ..Default::default() + }; + // Project config has no layout override capability. + let merged = MergedConfig::merge(&global, None); + assert!((merged.layout.left_pane_fraction - 0.30).abs() < f32::EPSILON); + } +} diff --git a/src-tauri/src/config/project.rs b/src-tauri/src/config/project.rs new file mode 100644 index 0000000..b7fd590 --- /dev/null +++ b/src-tauri/src/config/project.rs @@ -0,0 +1,160 @@ +//! Per-project configuration. + +use std::path::{Path, PathBuf}; + +use serde::{Deserialize, Serialize}; + +use super::{AppearanceOverride, ConfigError}; + +/// Reference IDs of tabs to restore when the project is next opened. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[serde(default)] +pub struct SessionConfig { + pub open_tabs: Vec, +} + +/// Per-project configuration, stored at `{repo}/.brittle/config.toml`. +/// +/// This file should be in the project's `.gitignore` — it holds local session +/// state and optional appearance overrides that are personal to this machine. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[serde(default)] +pub struct ProjectConfig { + /// Optional per-project appearance overrides. `None` means "use globals." + pub appearance: Option, + pub session: SessionConfig, +} + +impl ProjectConfig { + /// Load from `{repo_root}/.brittle/config.toml`. + /// + /// Returns `Default` if the file does not yet exist. + pub fn load(repo_root: &Path) -> Result { + Self::load_from(&project_config_path(repo_root)) + } + + /// Load from an explicit path. + /// + /// Returns `Default` if the file does not exist. + pub fn load_from(path: &Path) -> Result { + if !path.exists() { + return Ok(Self::default()); + } + let content = std::fs::read_to_string(path)?; + Ok(toml::from_str(&content)?) + } + + /// Save to `{repo_root}/.brittle/config.toml`. + pub fn save(&self, repo_root: &Path) -> Result<(), ConfigError> { + self.save_to(&project_config_path(repo_root)) + } + + /// Save to an explicit path, creating parent directories as needed. + pub fn save_to(&self, path: &Path) -> Result<(), ConfigError> { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(path, toml::to_string_pretty(self)?)?; + Ok(()) + } +} + +/// Returns the canonical path for a project's config file. +pub fn project_config_path(repo_root: &Path) -> PathBuf { + repo_root.join(".brittle").join("config.toml") +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::Theme; + use tempfile::TempDir; + + #[test] + fn project_config_defaults() { + let cfg = ProjectConfig::default(); + assert!(cfg.appearance.is_none()); + assert!(cfg.session.open_tabs.is_empty()); + } + + #[test] + fn project_config_round_trips() { + let cfg = ProjectConfig { + appearance: Some(AppearanceOverride { + theme: Some(Theme::Light), + font_size: Some(16), + }), + session: SessionConfig { + open_tabs: vec!["abc-123".to_string()], + }, + }; + let s = toml::to_string_pretty(&cfg).unwrap(); + let parsed: ProjectConfig = toml::from_str(&s).unwrap(); + assert_eq!(parsed, cfg); + } + + #[test] + fn empty_toml_uses_defaults() { + let cfg: ProjectConfig = toml::from_str("").unwrap(); + assert_eq!(cfg, ProjectConfig::default()); + } + + #[test] + fn partial_appearance_override_keeps_none_fields() { + // Only theme specified; font_size should remain None. + let toml = "[appearance]\ntheme = \"light\"\n"; + let cfg: ProjectConfig = toml::from_str(toml).unwrap(); + let ov = cfg.appearance.unwrap(); + assert_eq!(ov.theme, Some(Theme::Light)); + assert!(ov.font_size.is_none()); + } + + #[test] + fn load_from_nonexistent_path_returns_default() { + let tmp = TempDir::new().unwrap(); + let path = tmp.path().join("nope.toml"); + let cfg = ProjectConfig::load_from(&path).unwrap(); + assert_eq!(cfg, ProjectConfig::default()); + } + + #[test] + fn save_to_and_load_from_round_trip() { + let tmp = TempDir::new().unwrap(); + let path = tmp.path().join("config.toml"); + + let original = ProjectConfig { + appearance: Some(AppearanceOverride { + theme: Some(Theme::Dark), + font_size: None, + }), + session: SessionConfig { + open_tabs: vec!["ref-1".to_string()], + }, + }; + original.save_to(&path).unwrap(); + + let loaded = ProjectConfig::load_from(&path).unwrap(); + assert_eq!(loaded, original); + } + + #[test] + fn load_and_save_use_brittle_subdir() { + let tmp = TempDir::new().unwrap(); + let repo = tmp.path(); + + let original = ProjectConfig { + session: SessionConfig { + open_tabs: vec!["x".to_string()], + }, + ..Default::default() + }; + original.save(repo).unwrap(); + + assert!(repo.join(".brittle").join("config.toml").exists()); + + let loaded = ProjectConfig::load(repo).unwrap(); + assert_eq!(loaded, original); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..4c7d2fa --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,87 @@ +mod commands; +mod config; +mod pdf_protocol; +pub mod state; + +use state::AppState; + +pub fn run() { + tauri::Builder::default() + .manage(AppState::new()) + .setup(|app| { + #[cfg(debug_assertions)] + { + use tauri::Manager; + if let Some(win) = app.get_webview_window("main") { + win.open_devtools(); + } + } + Ok(()) + }) + .register_uri_scheme_protocol("brittle", |ctx, req| { + pdf_protocol::handle(ctx.app_handle(), &req) + }) + .invoke_handler(tauri::generate_handler![ + // config + commands::config::load_global_config, + commands::config::save_global_config, + commands::config::load_project_config, + commands::config::save_project_config, + commands::config::get_theme, + commands::config::set_theme, + commands::config::get_keybindings, + // repository + commands::repository::create_repository, + commands::repository::open_repository, + commands::repository::close_repository, + commands::repository::repository_root, + // reference + commands::reference::create_reference, + commands::reference::get_reference, + commands::reference::update_reference, + commands::reference::delete_reference, + commands::reference::list_references, + commands::reference::set_field, + commands::reference::remove_field, + commands::reference::search_references, + commands::reference::search_library_references, + commands::reference::list_library_references, + commands::reference::list_library_references_recursive, + // library + commands::library::create_library, + commands::library::get_library, + commands::library::rename_library, + commands::library::move_library, + commands::library::delete_library, + commands::library::force_delete_library, + commands::library::list_root_libraries, + commands::library::list_child_libraries, + commands::library::get_library_ancestors, + commands::library::add_to_library, + commands::library::remove_from_library, + commands::library::list_reference_libraries, + // annotation + commands::annotation::create_annotation, + commands::annotation::get_annotations, + commands::annotation::update_annotation, + commands::annotation::delete_annotation, + // pdf + commands::pdf::attach_pdf, + commands::pdf::get_pdf_path, + // snapshot + commands::snapshot::create_snapshot, + commands::snapshot::list_snapshots, + commands::snapshot::restore_snapshot, + commands::snapshot::has_uncommitted_changes, + commands::snapshot::discard_changes, + // bibtex + commands::bibtex::export_bibtex, + commands::bibtex::export_library_bibtex, + // window + commands::window::open_pdf_window, + commands::window::close_pdf_window, + commands::window::list_pdf_windows, + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..9b9e22a --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,5 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + brittle_app::run() +} diff --git a/src-tauri/src/pdf_protocol.rs b/src-tauri/src/pdf_protocol.rs new file mode 100644 index 0000000..14cb614 --- /dev/null +++ b/src-tauri/src/pdf_protocol.rs @@ -0,0 +1,320 @@ +//! Custom `brittle://` URI scheme handler. +//! +//! Routes: +//! `brittle://app/viewer?ref_id=` — the PDF viewer HTML page +//! `brittle://app/pdfjs/` — pdfjs-dist static assets +//! `brittle://app/pdf?ref_id=` — raw PDF bytes from the repository +//! +//! The pure routing and path-resolution logic lives in the `routing` sub-module +//! so it can be unit-tested without a running Tauri application. + +use std::path::PathBuf; +use tauri::{ + http::{header, Request, Response, StatusCode}, + AppHandle, Manager, Runtime, +}; + +use crate::state::AppState; + +// ── Embedded assets ─────────────────────────────────────────────────────────── + +static VIEWER_HTML: &[u8] = include_bytes!("pdf_viewer.html"); + +// ── Public API ──────────────────────────────────────────────────────────────── + +/// Entry point registered with `tauri::Builder::register_uri_scheme_protocol`. +/// +/// Generic over the Tauri runtime so the function can be used from a closure +/// in the builder without knowing the concrete runtime at compile time. +pub fn handle(app: &AppHandle, req: &Request>) -> Response> { + let uri = req.uri(); + + match routing::classify(uri.path(), uri.query()) { + routing::Route::Viewer { ref_id } => serve_viewer(&ref_id), + routing::Route::PdfjsAsset { rel_path } => { + if rel_path.contains("..") { + return response_403(); + } + serve_pdfjs_file(&pdfjs_root(app), &rel_path) + } + routing::Route::Pdf { ref_id } => serve_pdf(app, &ref_id), + routing::Route::NotFound => response_404(), + } +} + +// ── Route handlers ──────────────────────────────────────────────────────────── + +fn serve_viewer(ref_id: &str) -> Response> { + // Substitute the ref_id into the HTML template so the viewer knows which PDF to load. + let html = String::from_utf8_lossy(VIEWER_HTML) + .replace("ref_id=\"\"", &format!("ref_id=\"{}\"", ref_id)); + response_ok(html.into_bytes(), "text/html; charset=utf-8") +} + +fn serve_pdfjs_file(pdfjs_root: &std::path::Path, rel_path: &str) -> Response> { + let full_path = pdfjs_root.join(rel_path); + match std::fs::read(&full_path) { + Ok(bytes) => { + let mime = routing::mime_for_path(&full_path); + let mut resp = response_ok(bytes, mime); + resp.headers_mut().insert( + header::CACHE_CONTROL, + "public, max-age=3600".parse().unwrap(), + ); + resp + } + Err(_) => response_404(), + } +} + +fn serve_pdf(app: &AppHandle, ref_id: &str) -> Response> { + use brittle_core::{model::ids::ReferenceId, store::FsStore, Brittle}; + use uuid::Uuid; + + let uuid = match Uuid::parse_str(ref_id) { + Ok(u) => u, + Err(_) => return response_400("invalid ref_id: not a valid UUID"), + }; + let rid = ReferenceId::from(uuid); + + let state = app.state::(); + let pdf_path: Result = + state.with_repo_read(|b: &Brittle| b.get_pdf_path(rid)); + + match pdf_path { + Err(e) => response_404_msg(&e), + Ok(path) => match std::fs::read(&path) { + Ok(bytes) => { + let mut resp = response_ok(bytes, "application/pdf"); + resp.headers_mut() + .insert(header::CACHE_CONTROL, "no-store".parse().unwrap()); + resp + } + Err(e) => response_500(&e.to_string()), + }, + } +} + +// ── Path resolution ─────────────────────────────────────────────────────────── + +fn pdfjs_root(app: &AppHandle) -> PathBuf { + if cfg!(debug_assertions) { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("pdfjs") + .join("node_modules") + .join("pdfjs-dist") + } else { + app.path() + .resource_dir() + .unwrap_or_default() + .join("pdfjs-dist") + } +} + +// ── Response builders ───────────────────────────────────────────────────────── + +fn response_ok(body: Vec, content_type: &str) -> Response> { + Response::builder() + .header(header::CONTENT_TYPE, content_type) + .header(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .status(StatusCode::OK) + .body(body) + .unwrap() +} + +fn response_404() -> Response> { + Response::builder() + .status(StatusCode::NOT_FOUND) + .body(b"Not Found".to_vec()) + .unwrap() +} + +fn response_404_msg(msg: &str) -> Response> { + Response::builder() + .status(StatusCode::NOT_FOUND) + .body(msg.as_bytes().to_vec()) + .unwrap() +} + +fn response_400(msg: &str) -> Response> { + Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(msg.as_bytes().to_vec()) + .unwrap() +} + +fn response_403() -> Response> { + Response::builder() + .status(StatusCode::FORBIDDEN) + .body(b"Forbidden".to_vec()) + .unwrap() +} + +fn response_500(msg: &str) -> Response> { + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(msg.as_bytes().to_vec()) + .unwrap() +} + +// ── Pure routing logic (unit-testable) ─────────────────────────────────────── + +pub mod routing { + use std::path::Path; + + #[derive(Debug, PartialEq)] + pub enum Route { + Viewer { ref_id: String }, + PdfjsAsset { rel_path: String }, + Pdf { ref_id: String }, + NotFound, + } + + /// Classify a `brittle://app/{path}?{query}` request into a `Route`. + pub fn classify(path: &str, query: Option<&str>) -> Route { + let ref_id = extract_ref_id(query); + + if path == "/viewer" { + Route::Viewer { ref_id } + } else if let Some(rel) = path.strip_prefix("/pdfjs/") { + Route::PdfjsAsset { + rel_path: rel.to_owned(), + } + } else if path == "/pdf" { + Route::Pdf { ref_id } + } else { + Route::NotFound + } + } + + /// Extract the value of `ref_id=…` from a URL query string. + pub fn extract_ref_id(query: Option<&str>) -> String { + query + .unwrap_or("") + .split('&') + .find_map(|part| part.strip_prefix("ref_id=")) + .unwrap_or("") + .to_owned() + } + + /// Return the appropriate MIME type for a file path based on its extension. + pub fn mime_for_path(path: &Path) -> &'static str { + match path.extension().and_then(|e| e.to_str()) { + Some("js") => "application/javascript; charset=utf-8", + Some("mjs") => "application/javascript; charset=utf-8", + Some("css") => "text/css; charset=utf-8", + Some("html") => "text/html; charset=utf-8", + Some("pdf") => "application/pdf", + Some("woff2") => "font/woff2", + Some("woff") => "font/woff", + Some("png") => "image/png", + Some("jpg") | Some("jpeg") => "image/jpeg", + Some("svg") => "image/svg+xml", + Some("map") => "application/json", + _ => "application/octet-stream", + } + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn viewer_route() { + let r = classify("/viewer", Some("ref_id=abc-123")); + assert_eq!( + r, + Route::Viewer { + ref_id: "abc-123".into() + } + ); + } + + #[test] + fn viewer_route_no_ref_id() { + let r = classify("/viewer", None); + assert_eq!(r, Route::Viewer { ref_id: "".into() }); + } + + #[test] + fn pdfjs_asset_route_build_file() { + let r = classify("/pdfjs/build/pdf.min.js", None); + assert_eq!( + r, + Route::PdfjsAsset { + rel_path: "build/pdf.min.js".into() + } + ); + } + + #[test] + fn pdfjs_asset_route_nested() { + let r = classify("/pdfjs/web/pdf_viewer.css", None); + assert_eq!( + r, + Route::PdfjsAsset { + rel_path: "web/pdf_viewer.css".into() + } + ); + } + + #[test] + fn pdf_route() { + let r = classify("/pdf", Some("ref_id=01234567-89ab-cdef-0123-456789abcdef")); + assert_eq!( + r, + Route::Pdf { + ref_id: "01234567-89ab-cdef-0123-456789abcdef".into() + } + ); + } + + #[test] + fn unknown_paths_are_not_found() { + assert_eq!(classify("/unknown", None), Route::NotFound); + assert_eq!(classify("/", None), Route::NotFound); + assert_eq!(classify("/favicon.ico", None), Route::NotFound); + } + + #[test] + fn extract_ref_id_from_compound_query() { + let id = extract_ref_id(Some("foo=bar&ref_id=my-id&baz=1")); + assert_eq!(id, "my-id"); + } + + #[test] + fn extract_ref_id_missing_returns_empty() { + assert_eq!(extract_ref_id(None), ""); + assert_eq!(extract_ref_id(Some("foo=bar")), ""); + } + + #[test] + fn mime_for_js_files() { + assert!(mime_for_path(Path::new("pdf.min.js")).contains("javascript")); + } + + #[test] + fn mime_for_css_files() { + assert!(mime_for_path(Path::new("viewer.css")).contains("css")); + } + + #[test] + fn mime_for_unknown_extension() { + assert_eq!( + mime_for_path(Path::new("data.bin")), + "application/octet-stream" + ); + } + + #[test] + fn path_traversal_rel_path_contains_dotdot() { + // The handler rejects rel_paths containing ".."; verify the routing + // surfaces them so the handler can block them. + if let Route::PdfjsAsset { rel_path } = classify("/pdfjs/../secret", None) { + assert!(rel_path.contains("..")); + } else { + panic!("expected PdfjsAsset route"); + } + } + } +} diff --git a/src-tauri/src/pdf_viewer.html b/src-tauri/src/pdf_viewer.html new file mode 100644 index 0000000..b51bbff --- /dev/null +++ b/src-tauri/src/pdf_viewer.html @@ -0,0 +1,288 @@ + + + + + + PDF Viewer — Brittle + + + + +
+ +
+
+ + + + +
+ — / — + Loading PDF.js… +
+ +
+ + + + + diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs new file mode 100644 index 0000000..7cc225d --- /dev/null +++ b/src-tauri/src/state.rs @@ -0,0 +1,103 @@ +//! Managed application state for the Tauri backend. + +use brittle_core::{store::FsStore, Brittle, BrittleError}; +use std::sync::Mutex; + +/// Shared state held by the Tauri runtime for the lifetime of the application. +/// +/// `brittle` is `None` until the user opens or creates a repository. +pub struct AppState { + pub brittle: Mutex>>, +} + +impl Default for AppState { + fn default() -> Self { + Self::new() + } +} + +impl AppState { + pub fn new() -> Self { + Self { + brittle: Mutex::new(None), + } + } + + /// Run a closure with mutable access to the open repository. + /// + /// Errors if: + /// - the mutex is poisoned, + /// - no repository is currently open, or + /// - the operation itself returns an error. + pub fn with_repo(&self, f: F) -> Result + where + F: FnOnce(&mut Brittle) -> Result, + { + let mut guard = self + .brittle + .lock() + .map_err(|_| "internal: state lock poisoned".to_string())?; + let brittle = guard + .as_mut() + .ok_or_else(|| "no repository open".to_string())?; + f(brittle).map_err(|e| e.to_string()) + } + + /// Run a closure with read-only access to the open repository. + pub fn with_repo_read(&self, f: F) -> Result + where + F: FnOnce(&Brittle) -> Result, + { + let guard = self + .brittle + .lock() + .map_err(|_| "internal: state lock poisoned".to_string())?; + let brittle = guard + .as_ref() + .ok_or_else(|| "no repository open".to_string())?; + f(brittle).map_err(|e| e.to_string()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use brittle_core::EntryType; + + fn open_state() -> (AppState, tempfile::TempDir) { + let tmp = tempfile::tempdir().unwrap(); + let state = AppState::new(); + let brittle = Brittle::create(tmp.path()).unwrap(); + *state.brittle.lock().unwrap() = Some(brittle); + (state, tmp) + } + + #[test] + fn with_repo_fails_when_no_repo_open() { + let state = AppState::new(); + let result = state.with_repo(|_| Ok(())); + assert_eq!(result.unwrap_err(), "no repository open"); + } + + #[test] + fn with_repo_read_fails_when_no_repo_open() { + let state = AppState::new(); + let result = state.with_repo_read(|_| Ok(())); + assert_eq!(result.unwrap_err(), "no repository open"); + } + + #[test] + fn with_repo_succeeds_when_open() { + let (state, _tmp) = open_state(); + let result = state.with_repo(|b| b.create_reference("test2024", EntryType::Article)); + assert!(result.is_ok()); + assert_eq!(result.unwrap().cite_key, "test2024"); + } + + #[test] + fn with_repo_read_can_list_references() { + let (state, _tmp) = open_state(); + let result = state.with_repo_read(|b| b.list_references()); + assert!(result.is_ok()); + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..02bb10e --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "Brittle", + "version": "0.1.0", + "identifier": "dev.brittle.app", + "build": { + "beforeDevCommand": { + "script": "trunk serve", + "cwd": "." + }, + "beforeBuildCommand": { + "script": "trunk build --release", + "cwd": "." + }, + "devUrl": "http://localhost:1420", + "frontendDist": "../dist" + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "label": "main", + "title": "Brittle", + "width": 1200, + "height": 800, + "minWidth": 800, + "minHeight": 600 + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [] + } +} diff --git a/src-tauri/tests/commands.rs b/src-tauri/tests/commands.rs new file mode 100644 index 0000000..f90dbad --- /dev/null +++ b/src-tauri/tests/commands.rs @@ -0,0 +1,269 @@ +//! Integration tests for the Tauri command layer. +//! +//! These tests exercise `AppState` and the command logic end-to-end using a +//! real `Brittle` repository in a temp directory. Tauri IPC is not +//! involved — the functions under test are plain Rust. + +use brittle_app::state::AppState; +use brittle_core::{Brittle, EntryType, Person}; + +fn open_state() -> (AppState, tempfile::TempDir) { + let tmp = tempfile::tempdir().unwrap(); + let state = AppState::new(); + let brittle = Brittle::create(tmp.path()).unwrap(); + *state.brittle.lock().unwrap() = Some(brittle); + (state, tmp) +} + +// ── AppState ───────────────────────────────────────────────────────────────── + +#[test] +fn no_repo_open_returns_error() { + let state = AppState::new(); + let err = state.with_repo(|_| Ok(())).unwrap_err(); + assert_eq!(err, "no repository open"); +} + +#[test] +fn with_repo_propagates_brittle_errors() { + let (state, _tmp) = open_state(); + // Trying to get a non-existent reference propagates the StoreError. + let err = state + .with_repo_read(|b| { + use brittle_core::model::ids::ReferenceId; + b.get_reference(ReferenceId::new()) + }) + .unwrap_err(); + assert!(!err.is_empty()); +} + +// ── Repository lifecycle ────────────────────────────────────────────────────── + +#[test] +fn create_and_reopen_repository() { + let tmp = tempfile::tempdir().unwrap(); + let state = AppState::new(); + + // Create + { + let brittle = Brittle::create(tmp.path()).unwrap(); + *state.brittle.lock().unwrap() = Some(brittle); + } + + // Close + *state.brittle.lock().unwrap() = None; + assert!(state.with_repo_read(|_| Ok(())).is_err()); + + // Reopen + { + let brittle = Brittle::open(tmp.path()).unwrap(); + *state.brittle.lock().unwrap() = Some(brittle); + } + assert!(state.with_repo_read(|_| Ok(())).is_ok()); +} + +// ── Reference CRUD ─────────────────────────────────────────────────────────── + +#[test] +fn create_and_list_references() { + let (state, _tmp) = open_state(); + + state + .with_repo(|b| b.create_reference("turing1950", EntryType::Article)) + .unwrap(); + state + .with_repo(|b| b.create_reference("knuth1984", EntryType::Book)) + .unwrap(); + + let refs = state.with_repo_read(|b| b.list_references()).unwrap(); + assert_eq!(refs.len(), 2); + let keys: Vec<&str> = refs.iter().map(|r| r.cite_key.as_str()).collect(); + assert!(keys.contains(&"turing1950")); + assert!(keys.contains(&"knuth1984")); +} + +#[test] +fn delete_reference_removes_it() { + let (state, _tmp) = open_state(); + + let r = state + .with_repo(|b| b.create_reference("gone2024", EntryType::Misc)) + .unwrap(); + + state.with_repo(|b| b.delete_reference(r.id)).unwrap(); + + let refs = state.with_repo_read(|b| b.list_references()).unwrap(); + assert!(refs.is_empty()); +} + +#[test] +fn set_and_remove_field() { + let (state, _tmp) = open_state(); + + let r = state + .with_repo(|b| b.create_reference("fields2024", EntryType::Article)) + .unwrap(); + + state + .with_repo(|b| b.set_field(r.id, "title", "A Test Title")) + .unwrap(); + + let fetched = state.with_repo_read(|b| b.get_reference(r.id)).unwrap(); + assert_eq!( + fetched.fields.get("title").map(String::as_str), + Some("A Test Title") + ); + + state.with_repo(|b| b.remove_field(r.id, "title")).unwrap(); + + let fetched2 = state.with_repo_read(|b| b.get_reference(r.id)).unwrap(); + assert!(!fetched2.fields.contains_key("title")); +} + +#[test] +fn search_references_filters_by_query() { + let (state, _tmp) = open_state(); + + state + .with_repo(|b| b.create_reference("turing1950", EntryType::Article)) + .unwrap(); + state + .with_repo(|b| b.create_reference("knuth1984", EntryType::Book)) + .unwrap(); + + let results = state + .with_repo_read(|b| b.search_references("turing")) + .unwrap(); + assert_eq!(results.len(), 1); + assert_eq!(results[0].cite_key, "turing1950"); +} + +// ── Library ─────────────────────────────────────────────────────────────────── + +#[test] +fn create_nested_libraries_and_query_hierarchy() { + let (state, _tmp) = open_state(); + + let root = state.with_repo(|b| b.create_library("Root", None)).unwrap(); + let child = state + .with_repo(|b| b.create_library("Child", Some(root.id))) + .unwrap(); + + let roots = state.with_repo_read(|b| b.list_root_libraries()).unwrap(); + assert_eq!(roots.len(), 1); + assert_eq!(roots[0].id, root.id); + + let children = state + .with_repo_read(|b| b.list_child_libraries(root.id)) + .unwrap(); + assert_eq!(children.len(), 1); + assert_eq!(children[0].id, child.id); + + let ancestors = state + .with_repo_read(|b| b.get_library_ancestors(child.id)) + .unwrap(); + assert_eq!(ancestors.len(), 1); + assert_eq!(ancestors[0].id, root.id); +} + +#[test] +fn add_reference_to_library_and_query() { + let (state, _tmp) = open_state(); + + let r = state + .with_repo(|b| b.create_reference("member2024", EntryType::Article)) + .unwrap(); + let lib = state.with_repo(|b| b.create_library("Lib", None)).unwrap(); + + state.with_repo(|b| b.add_to_library(lib.id, r.id)).unwrap(); + + let members = state + .with_repo_read(|b| b.list_library_references(lib.id)) + .unwrap(); + assert_eq!(members.len(), 1); + assert_eq!(members[0].id, r.id); +} + +#[test] +fn force_delete_library_removes_subtree() { + let (state, _tmp) = open_state(); + + let root = state.with_repo(|b| b.create_library("Root", None)).unwrap(); + state + .with_repo(|b| b.create_library("Child", Some(root.id))) + .unwrap(); + + state + .with_repo(|b| b.force_delete_library(root.id)) + .unwrap(); + + let all = state.with_repo_read(|b| b.list_root_libraries()).unwrap(); + assert!(all.is_empty()); +} + +// ── BibTeX export ───────────────────────────────────────────────────────────── + +#[test] +fn export_library_bibtex_contains_entries() { + let (state, _tmp) = open_state(); + + let mut r = state + .with_repo(|b| b.create_reference("turing1950", EntryType::Article)) + .unwrap(); + r.authors.push(Person::new("Turing")); + r.fields.insert( + "title".into(), + "Computing Machinery and Intelligence".into(), + ); + r.fields.insert("journal".into(), "Mind".into()); + r.fields.insert("year".into(), "1950".into()); + let r = state.with_repo(|b| b.update_reference(r)).unwrap(); + + let lib = state.with_repo(|b| b.create_library("CS", None)).unwrap(); + state.with_repo(|b| b.add_to_library(lib.id, r.id)).unwrap(); + + let (bibtex, errors) = state + .with_repo_read(|b| b.export_library_bibtex(lib.id)) + .unwrap(); + + assert!(errors.is_empty()); + assert!(bibtex.contains("@article{turing1950,")); + assert!(bibtex.contains("Computing Machinery and Intelligence")); +} + +// ── Snapshot ────────────────────────────────────────────────────────────────── + +#[test] +fn snapshot_and_discard_changes() { + let (state, _tmp) = open_state(); + + state + .with_repo(|b| b.create_reference("snap2024", EntryType::Misc)) + .unwrap(); + + let snap = state.with_repo(|b| b.create_snapshot("baseline")).unwrap(); + assert!(!snap.id.is_empty()); + + let snapshots = state.with_repo_read(|b| b.list_snapshots()).unwrap(); + assert!(snapshots.iter().any(|s| s.message == "baseline")); + + // Delete the reference (uncommitted change). + let r_id = state + .with_repo_read(|b| b.list_references()) + .unwrap() + .into_iter() + .next() + .unwrap() + .id; + state.with_repo(|b| b.delete_reference(r_id)).unwrap(); + + assert!(state + .with_repo_read(|b| b.has_uncommitted_changes()) + .unwrap()); + + // Discard → reference comes back. + state.with_repo(|b| b.discard_changes()).unwrap(); + + let refs = state.with_repo_read(|b| b.list_references()).unwrap(); + assert_eq!(refs.len(), 1); +} diff --git a/src/Cargo.lock b/src/Cargo.lock new file mode 100644 index 0000000..fdc957b --- /dev/null +++ b/src/Cargo.lock @@ -0,0 +1,1988 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "any_spawner" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41058deaa38c9d9dd933d6d238d825227cffa668e2839b52879f6619c63eee3b" +dependencies = [ + "futures", + "thiserror 2.0.18", + "wasm-bindgen-futures", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[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-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "attribute-derive" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05832cdddc8f2650cc2cc187cc2e952b8c133a48eb055f35211f61ee81502d77" +dependencies = [ + "attribute-derive-macro", + "derive-where", + "manyhow", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "attribute-derive-macro" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7cdbbd4bd005c5d3e2e9c885e6fa575db4f4a3572335b974d8db853b6beb61" +dependencies = [ + "collection_literals", + "interpolator", + "manyhow", + "proc-macro-utils", + "proc-macro2", + "quote", + "quote-use", + "syn", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "brittle-keymap" +version = "0.1.0" + +[[package]] +name = "brittle-ui" +version = "0.1.0" +dependencies = [ + "brittle-keymap", + "js-sys", + "leptos", + "serde", + "serde-wasm-bindgen", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "codee" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9dbbdc4b4d349732bc6690de10a9de952bd39ba6a065c586e26600b6b0b91f5" +dependencies = [ + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "collection_literals" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084" + +[[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 = "config" +version = "0.15.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68cfe19cd7d23ffde002c24ffa5cda73931913ef394d5eaaa32037dc940c0c" +dependencies = [ + "convert_case 0.6.0", + "pathdiff", + "serde_core", + "toml", + "winnow", +] + +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "const_str_slice_concat" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67855af358fcb20fac58f9d714c94e2b228fe5694c1c9b4ead4a366343eda1b" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "derive-where" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "drain_filter_polyfill" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "either_of" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f7f86eef3a7e4b9c2107583dbbbe3d9535c4b800796faf1774b82ba22033da" +dependencies = [ + "paste", + "pin-project-lite", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[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 = "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" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[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-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[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", + "wasip2", + "wasip3", +] + +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "guardian" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[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 = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "hydration_context" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d35485b3dcbf7e044b8f28c73f04f13e7b509c2466fd10cb2a8a447e38f8a93a" +dependencies = [ + "futures", + "once_cell", + "or_poisoned", + "pin-project-lite", + "serde", + "throw_error", +] + +[[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 = "interpolator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[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 = "leptos" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b8731cb00f3f0894058155410b95c8955b17273181d2bc72600ab84edd24f1" +dependencies = [ + "any_spawner", + "cfg-if", + "either_of", + "futures", + "hydration_context", + "leptos_config", + "leptos_dom", + "leptos_hot_reload", + "leptos_macro", + "leptos_server", + "oco_ref", + "or_poisoned", + "paste", + "reactive_graph", + "rustc-hash", + "send_wrapper", + "serde", + "serde_qs", + "server_fn", + "slotmap", + "tachys", + "thiserror 2.0.18", + "throw_error", + "typed-builder", + "typed-builder-macro", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_config" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bae3e0ead5a7a814c8340eef7cb8b6cba364125bd8174b15dc9fe1b3cab7e03" +dependencies = [ + "config", + "regex", + "serde", + "thiserror 2.0.18", + "typed-builder", +] + +[[package]] +name = "leptos_dom" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89d4eb263bd5a9e7c49f780f17063f15aca56fd638c90b9dfd5f4739152e87d" +dependencies = [ + "js-sys", + "or_poisoned", + "reactive_graph", + "send_wrapper", + "tachys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_hot_reload" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e80219388501d99b246f43b6e7d08a28f327cdd34ba630a35654d917f3e1788e" +dependencies = [ + "anyhow", + "camino", + "indexmap", + "parking_lot", + "proc-macro2", + "quote", + "rstml", + "serde", + "syn", + "walkdir", +] + +[[package]] +name = "leptos_macro" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e621f8f5342b9bdc93bb263b839cee7405027a74560425a2dabea9de7952b1fd" +dependencies = [ + "attribute-derive", + "cfg-if", + "convert_case 0.7.1", + "html-escape", + "itertools", + "leptos_hot_reload", + "prettyplease", + "proc-macro-error2", + "proc-macro2", + "quote", + "rstml", + "server_fn_macro", + "syn", + "uuid", +] + +[[package]] +name = "leptos_server" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66985242812ec95e224fb48effe651ba02728beca92c461a9464c811a71aab11" +dependencies = [ + "any_spawner", + "base64", + "codee", + "futures", + "hydration_context", + "or_poisoned", + "reactive_graph", + "send_wrapper", + "serde", + "serde_json", + "server_fn", + "tachys", +] + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "linear-map" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "manyhow" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "manyhow-macros" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "next_tuple" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60993920e071b0c9b66f14e2b32740a4e27ffc82854dcd72035887f336a09a28" + +[[package]] +name = "oco_ref" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed0423ff9973dea4d6bd075934fdda86ebb8c05bdf9d6b0507067d4a1226371d" +dependencies = [ + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "or_poisoned" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c04f5d74368e4d0dfe06c45c8627c81bd7c317d52762d118fb9b3076f6420fd" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[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-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro-utils" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + +[[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 = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", + "yansi", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quote-use" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e" +dependencies = [ + "quote", + "quote-use-macros", +] + +[[package]] +name = "quote-use-macros" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "reactive_graph" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a0ccddbc11a648bd09761801dac9e3f246ef7641130987d6120fced22515e6" +dependencies = [ + "any_spawner", + "async-lock", + "futures", + "guardian", + "hydration_context", + "or_poisoned", + "pin-project-lite", + "rustc-hash", + "send_wrapper", + "serde", + "slotmap", + "thiserror 2.0.18", + "web-sys", +] + +[[package]] +name = "reactive_stores" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aadc7c19e3a360bf19cd595d2dc8b58ce67b9240b95a103fbc1317a8ff194237" +dependencies = [ + "guardian", + "itertools", + "or_poisoned", + "paste", + "reactive_graph", + "reactive_stores_macro", + "rustc-hash", +] + +[[package]] +name = "reactive_stores_macro" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "221095cb028dc51fbc2833743ea8b1a585da1a2af19b440b3528027495bf1f2d" +dependencies = [ + "convert_case 0.7.1", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rstml" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61cf4616de7499fc5164570d40ca4e1b24d231c6833a88bff0fe00725080fd56" +dependencies = [ + "derive-where", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", + "syn_derive", + "thiserror 2.0.18", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" +dependencies = [ + "futures-core", +] + +[[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-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[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_qs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "serde_spanned" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" +dependencies = [ + "serde_core", +] + +[[package]] +name = "server_fn" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d05a9e3fd8d7404985418db38c6617cc793a1a27f398d4fbc9dfe8e41b804e6" +dependencies = [ + "bytes", + "const_format", + "dashmap", + "futures", + "gloo-net", + "http", + "js-sys", + "once_cell", + "pin-project-lite", + "send_wrapper", + "serde", + "serde_json", + "serde_qs", + "server_fn_macro_default", + "thiserror 2.0.18", + "throw_error", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504b35e883267b3206317b46d02952ed7b8bf0e11b2e209e2eb453b609a5e052" +dependencies = [ + "const_format", + "convert_case 0.6.0", + "proc-macro2", + "quote", + "syn", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro_default" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb8b274f568c94226a8045668554aace8142a59b8bca5414ac5a79627c825568" +dependencies = [ + "server_fn_macro", + "syn", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[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 = "syn_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb066a04799e45f5d582e8fc6ec8e6d6896040d00898eb4e6a835196815b219" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[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 = "tachys" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f66c3b70c32844a6f1e2943c72a33ebb777ad6acbeb20d1329d62e3a7806d6ec" +dependencies = [ + "any_spawner", + "async-trait", + "const_str_slice_concat", + "drain_filter_polyfill", + "dyn-clone", + "either_of", + "futures", + "html-escape", + "indexmap", + "itertools", + "js-sys", + "linear-map", + "next_tuple", + "oco_ref", + "once_cell", + "or_poisoned", + "parking_lot", + "paste", + "reactive_graph", + "reactive_stores", + "rustc-hash", + "send_wrapper", + "slotmap", + "throw_error", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "throw_error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ef8bf264c6ae02a065a4a16553283f0656bd6266fc1fcb09fd2e6b5e91427b" +dependencies = [ + "pin-project-lite", +] + +[[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 = "1.1.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8195ca05e4eb728f4ba94f3e3291661320af739c4e43779cbdfae82ab239fcc" +dependencies = [ + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" +dependencies = [ + "winnow", +] + +[[package]] +name = "typed-builder" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9d30e3a08026c78f246b173243cf07b3696d274debd26680773b6773c2afc7" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c36781cc0e46a83726d9879608e4cf6c2505237e263a8eb8c24502989cfdb28" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[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", +] + +[[package]] +name = "utf8-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" + +[[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 = [ + "getrandom", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[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 = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[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 = "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 = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +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 = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/src/Cargo.toml b/src/Cargo.toml new file mode 100644 index 0000000..74d7d23 --- /dev/null +++ b/src/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "brittle-ui" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +brittle-keymap = { path = "../brittle-keymap" } +js-sys = "0.3" +leptos = { version = "0.7", features = ["csr"] } +wasm-bindgen = "0.2" +serde = { version = "1", features = ["derive"] } +serde-wasm-bindgen = "0.6" +wasm-bindgen-futures = "0.4" +web-sys = { version = "0.3", features = ["DataTransfer", "DragEvent", "KeyboardEvent"] } + +[dev-dependencies] +serde_json = "1" diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..f9a3e00 --- /dev/null +++ b/src/index.html @@ -0,0 +1,11 @@ + + + + + + Brittle + + + + + diff --git a/src/src/command_bar.rs b/src/src/command_bar.rs new file mode 100644 index 0000000..39832fe --- /dev/null +++ b/src/src/command_bar.rs @@ -0,0 +1,147 @@ +//! The command / search bar shown at the bottom of the screen. +//! +//! Visible only in [`AppMode::Command`] and [`AppMode::Search`] modes. +//! Handles its own keyboard events (Enter, Escape) and stops propagation +//! so the global keymap does not also process those keys. + +use leptos::prelude::*; +use web_sys::KeyboardEvent; + +use crate::{commands::{self, CommandEffect}, mode::AppMode, ThemeContext}; + +/// Shared search query, provided in context so other components can read it. +/// +/// Set by the search bar when the user commits a search (``). +/// Cleared when the user cancels with ``. +#[derive(Clone, Copy)] +pub struct SearchQuery(pub RwSignal); + +pub fn provide_search_query() -> RwSignal { + let sig = RwSignal::new(String::new()); + provide_context(SearchQuery(sig)); + sig +} + +/// The command/search bar component. +/// +/// Pass the application mode signal; the bar appears/disappears reactively. +#[component] +pub fn CommandBar(mode: RwSignal) -> impl IntoView { + let input_ref = NodeRef::::new(); + let (input_val, set_input_val) = signal(String::new()); + let (status_msg, set_status_msg) = signal(Option::::None); + + let search_query = use_context::().map(|sq| sq.0); + let theme = use_context::().map(|tc| tc.0); + let reload_trigger = use_context::().map(|r| r.0); + + // Clear input and status when the mode changes; autofocus on open. + Effect::new(move |_| { + let m = mode.get(); + set_input_val.set(String::new()); + set_status_msg.set(None); + if m != AppMode::Normal { + if let Some(el) = input_ref.get() { + let _ = el.focus(); + } + } + }); + + let prefix = move || match mode.get() { + AppMode::Normal => "", + AppMode::Command => ":", + AppMode::Search => "/", + }; + + let on_keydown = move |ev: KeyboardEvent| { + match ev.key().as_str() { + "Escape" => { + ev.prevent_default(); + ev.stop_propagation(); + // Cancel: clear search query and return to normal. + if let Some(sq) = search_query { + sq.set(String::new()); + } + mode.set(AppMode::Normal); + } + "Enter" => { + ev.prevent_default(); + ev.stop_propagation(); + let val = input_val.get_untracked(); + match mode.get_untracked() { + AppMode::Command => { + let outcome = commands::dispatch(&val); + if let Some(msg) = outcome.message { + // Show error; stay in command mode. + set_status_msg.set(Some(msg)); + return; + } + if let Some(effect) = outcome.effect { + match effect { + CommandEffect::SetTheme(t) => { + if let Some(sig) = theme { + sig.set(t.clone()); + } + leptos::task::spawn_local(async move { + let _ = crate::tauri::set_theme(&t).await; + }); + } + CommandEffect::OpenRepository(path) => { + let reload = reload_trigger; + leptos::task::spawn_local(async move { + match crate::tauri::open_repository(&path).await { + Ok(()) => { + if let Some(t) = reload { + t.update(|n| *n += 1); + } + mode.set(AppMode::Normal); + } + Err(e) => set_status_msg.set(Some(e)), + } + }); + return; // stay open until async resolves + } + } + } + mode.set(AppMode::Normal); + } + AppMode::Search => { + // Commit the search query and return to normal. + if let Some(sq) = search_query { + sq.set(val); + } + mode.set(AppMode::Normal); + } + AppMode::Normal => {} + } + } + _ => {} + } + }; + + let on_input = move |ev: web_sys::Event| { + set_input_val.set(event_target_value(&ev)); + set_status_msg.set(None); + }; + + view! { + +
+ {prefix} + + {move || status_msg.get().map(|m| view! { + " — "{m} + })} +
+
+ } +} diff --git a/src/src/commands.rs b/src/src/commands.rs new file mode 100644 index 0000000..225536d --- /dev/null +++ b/src/src/commands.rs @@ -0,0 +1,128 @@ +//! Command dispatch for command mode (the `:` prompt). +//! +//! Each command returns a [`DispatchOutcome`] containing an optional status +//! message and an optional side-effect for the UI layer to perform. +//! A `None` message means the command succeeded silently; `Some(msg)` is shown +//! in the command bar as an error or confirmation. +//! +//! Commands are simple strings; arguments follow a space: `:theme dark`. + +/// A side-effect that the UI layer must perform after a successful command. +pub enum CommandEffect { + /// Apply and persist the given theme (`"dark"` or `"light"`). + SetTheme(String), + /// Open the repository at the given filesystem path. + OpenRepository(String), +} + +/// Result of dispatching a command. +pub struct DispatchOutcome { + /// Message to show in the command bar; `None` = silent success. + pub message: Option, + /// Side-effect for the UI layer to carry out. + pub effect: Option, +} + +impl DispatchOutcome { + fn ok() -> Self { + Self { message: None, effect: None } + } + fn err(msg: impl Into) -> Self { + Self { message: Some(msg.into()), effect: None } + } + fn with_effect(effect: CommandEffect) -> Self { + Self { message: None, effect: Some(effect) } + } +} + +/// Execute a command entered in command mode. +pub fn dispatch(input: &str) -> DispatchOutcome { + let input = input.trim(); + if input.is_empty() { + return DispatchOutcome::ok(); + } + + let (cmd, args) = input + .split_once(' ') + .map(|(c, a)| (c, a.trim())) + .unwrap_or((input, "")); + + match cmd { + // ── Lifecycle ──────────────────────────────────────────────────────── + "q" | "quit" => { + // TODO Phase 8: call tauri::window::close() + DispatchOutcome::ok() + } + + // ── Theme ──────────────────────────────────────────────────────────── + "theme" => match args { + "dark" | "light" => { + DispatchOutcome::with_effect(CommandEffect::SetTheme(args.to_string())) + } + "" => DispatchOutcome::err("usage: theme "), + a => DispatchOutcome::err(format!("unknown theme '{a}'")), + }, + + // ── Repository ─────────────────────────────────────────────────────── + "open" => { + if args.is_empty() { + DispatchOutcome::err("usage: open ") + } else { + DispatchOutcome::with_effect(CommandEffect::OpenRepository(args.to_string())) + } + } + + _ => DispatchOutcome::err(format!("unknown command '{cmd}'")), + } +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_input_is_silent() { + assert!(dispatch("").message.is_none()); + assert!(dispatch(" ").message.is_none()); + } + + #[test] + fn quit_is_silent() { + assert!(dispatch("q").message.is_none()); + assert!(dispatch("quit").message.is_none()); + } + + #[test] + fn valid_theme_produces_effect() { + let dark = dispatch("theme dark"); + assert!(dark.message.is_none()); + assert!(matches!(&dark.effect, Some(CommandEffect::SetTheme(t)) if t == "dark")); + + let light = dispatch("theme light"); + assert!(light.message.is_none()); + assert!(matches!(&light.effect, Some(CommandEffect::SetTheme(t)) if t == "light")); + } + + #[test] + fn invalid_theme_returns_error() { + let msg = dispatch("theme solarized").message; + assert!(msg.is_some()); + assert!(msg.unwrap().contains("solarized")); + } + + #[test] + fn theme_without_args_returns_usage() { + let msg = dispatch("theme").message; + assert!(msg.is_some()); + assert!(msg.unwrap().contains("usage")); + } + + #[test] + fn unknown_command_returns_error() { + let msg = dispatch("frobnicate").message; + assert!(msg.is_some()); + assert!(msg.unwrap().contains("frobnicate")); + } +} diff --git a/src/src/lib_tab.rs b/src/src/lib_tab.rs new file mode 100644 index 0000000..e4b8a10 --- /dev/null +++ b/src/src/lib_tab.rs @@ -0,0 +1,337 @@ +//! The Library tab: three-pane layout with tree, list, and detail panes. + +use std::collections::{HashMap, HashSet}; + +use brittle_keymap::actions; +use leptos::prelude::*; +use leptos::task::spawn_local; + +use crate::{ + command_bar::SearchQuery, + lib_tree::{flatten_tree, LibraryTree, TreeRow}, + models::{Library, LibraryId, Reference, ReferenceId, ReferenceSummary}, + pub_detail::PubDetail, + pub_list::PubList, + ActionEvent, +}; + +// ── Pane enum ───────────────────────────────────────────────────────────────── + +/// Which of the three panes currently has keyboard focus. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Pane { + Tree, + List, + Detail, +} + +impl Pane { + pub fn next(self) -> Self { + match self { + Pane::Tree => Pane::List, + Pane::List => Pane::Detail, + Pane::Detail => Pane::Tree, + } + } + + pub fn prev(self) -> Self { + match self { + Pane::Tree => Pane::Detail, + Pane::List => Pane::Tree, + Pane::Detail => Pane::List, + } + } +} + +// ── Component ───────────────────────────────────────────────────────────────── + +/// Root component for the Library tab. +/// +/// Owns all UI state, wires keymap actions, and drives async data loading. +#[component] +pub fn LibTab() -> impl IntoView { + // ── Context signals ─────────────────────────────────────────────────────── + let keymap_action = use_context::() + .expect("KeymapAction context missing") + .0; + + let search_query = use_context::() + .map(|sq| sq.0) + .unwrap_or_else(|| RwSignal::new(String::new())); + + // ── UI state ────────────────────────────────────────────────────────────── + let focused = RwSignal::new(Pane::Tree); + + // Tree state + let root_libs = RwSignal::new(Vec::::new()); + let children_cache = RwSignal::new(HashMap::>::new()); + let expanded = RwSignal::new(HashSet::::new()); + let tree_cursor = RwSignal::new(0usize); + + // List state + let list_items = RwSignal::new(Vec::::new()); + let list_cursor = RwSignal::new(0usize); + + // Detail state + let detail_ref = RwSignal::new(Option::::None); + + // ── Derived / computed ──────────────────────────────────────────────────── + + // Flattened visible tree rows (recomputed when tree data or expand set changes). + let tree_rows = Memo::new(move |_| { + flatten_tree(&root_libs.get(), &children_cache.get(), &expanded.get(), 0) + }); + + // The library selected by the tree cursor. + let selected_library = Memo::new(move |_| { + let pos = tree_cursor.get(); + tree_rows.with(|rows| rows.get(pos).map(|r| LibraryId(r.id.clone()))) + }); + + // The reference ID selected by the list cursor. + let selected_ref_id = Memo::new(move |_| { + let pos = list_cursor.get(); + list_items.with(|items| items.get(pos).map(|r| r.id.clone())) + }); + + // ── Initial data load (and reload on repository change) ─────────────────── + let reload_trigger = use_context::().map(|r| r.0); + Effect::new(move |_| { + if let Some(t) = reload_trigger { t.get(); } // track the trigger + spawn_local(async move { + match crate::tauri::list_root_libraries().await { + Ok(libs) => root_libs.set(libs), + Err(e) => leptos::logging::log!("load libraries: {e}"), + } + }); + }); + + // ── Reactive data loads ─────────────────────────────────────────────────── + + // Reload list whenever the selected library or search query changes. + Effect::new(move |_| { + let lib_id = selected_library.get(); + let query = search_query.get(); + spawn_local(async move { + let result = match (&lib_id, query.is_empty()) { + (Some(id), true) => crate::tauri::list_library_references_recursive(id).await, + (Some(id), false) => crate::tauri::search_library_references(id, &query).await, + (None, true) => crate::tauri::list_references().await, + (None, false) => crate::tauri::search_references(&query).await, + }; + match result { + Ok(items) => list_items.set(items), + Err(e) => { + leptos::logging::error!("load publications: {e}"); + list_items.set(vec![]); + } + } + list_cursor.set(0); + }); + }); + + // Load full reference when list cursor moves. + Effect::new(move |_| { + let ref_id: Option = selected_ref_id.get(); + spawn_local(async move { + let loaded = match ref_id { + Some(id) => crate::tauri::get_reference(&id).await.ok(), + None => None, + }; + detail_ref.set(loaded); + }); + }); + + // ── Keymap wiring ───────────────────────────────────────────────────────── + Effect::new(move |_| { + let Some(ev) = keymap_action.get() else { return }; + + // ACTION_OPEN in the List or Detail pane opens a PDF tab for the selected reference. + if ev.name == actions::ACTION_OPEN + && matches!(focused.get_untracked(), Pane::List | Pane::Detail) + { + if let Some(ref_id) = selected_ref_id.get_untracked() { + let title = detail_ref.with_untracked(|r| { + r.as_ref() + .map(|r| r.cite_key.clone()) + .unwrap_or_else(|| ref_id.0.clone()) + }); + if let Some(ctx) = use_context::() { + ctx.0.set(Some(crate::PdfOpenRequest { + ref_id: ref_id.0.clone(), + title, + })); + } + } + return; + } + + handle_action( + &ev, + focused, + TreeState { cursor: tree_cursor, rows: tree_rows, expanded }, + ListState { cursor: list_cursor, items: list_items }, + ); + }); + + // ── View ────────────────────────────────────────────────────────────────── + view! { +
+
+ +
+
+ +
+
+ +
+
+ } +} + +// ── Action handler ──────────────────────────────────────────────────────────── + +struct TreeState { + cursor: RwSignal, + rows: Memo>, + expanded: RwSignal>, +} + +struct ListState { + cursor: RwSignal, + items: RwSignal>, +} + +fn handle_action( + ev: &ActionEvent, + focused: RwSignal, + tree: TreeState, + list: ListState, +) { + let count = (ev.count as usize).max(1); + let cur = focused.get_untracked(); + + match ev.name.as_str() { + // ── Focus switching ─────────────────────────────────────────────────── + actions::FOCUS_LEFT => focused.set(Pane::Tree), + actions::FOCUS_CENTER => focused.set(Pane::List), + actions::FOCUS_RIGHT => focused.set(Pane::Detail), + actions::FOCUS_NEXT => focused.update(|p| *p = p.next()), + actions::FOCUS_PREV => focused.update(|p| *p = p.prev()), + + // ── Tree navigation ─────────────────────────────────────────────────── + actions::NAV_DOWN if cur == Pane::Tree => { + let len = tree.rows.with_untracked(Vec::len); + if len > 0 { + tree.cursor.update(|c| *c = (*c + count).min(len - 1)); + } + } + actions::NAV_UP if cur == Pane::Tree => { + tree.cursor.update(|c| *c = c.saturating_sub(count)); + } + actions::NAV_TOP if cur == Pane::Tree => tree.cursor.set(0), + actions::NAV_BOTTOM if cur == Pane::Tree => { + let len = tree.rows.with_untracked(Vec::len); + if len > 0 { + tree.cursor.set(len - 1); + } + } + actions::NAV_PAGE_DOWN if cur == Pane::Tree => { + let len = tree.rows.with_untracked(Vec::len); + if len > 0 { + tree.cursor.update(|c| *c = (*c + 10 * count).min(len - 1)); + } + } + actions::NAV_PAGE_UP if cur == Pane::Tree => { + tree.cursor.update(|c| *c = c.saturating_sub(10 * count)); + } + + // ── Tree expand / collapse ──────────────────────────────────────────── + n if (n == actions::TREE_EXPAND || n == actions::ACTION_OPEN) && cur == Pane::Tree => { + tree_expand(tree.cursor, tree.rows, tree.expanded); + } + actions::TREE_COLLAPSE if cur == Pane::Tree => { + tree_collapse(tree.cursor, tree.rows, tree.expanded); + } + actions::TREE_TOGGLE if cur == Pane::Tree => { + let id = tree.rows.with_untracked(|rows| { + rows.get(tree.cursor.get_untracked()).map(|r| r.id.clone()) + }); + if let Some(id) = id { + let is_open = tree.expanded.with_untracked(|e| e.contains(&id)); + if is_open { + tree.expanded.update(|e| { e.remove(&id); }); + } else { + tree.expanded.update(|e| { e.insert(id.clone()); }); + // Child loading is handled reactively by LibraryTree's Effect. + } + } + } + + // ── List navigation ─────────────────────────────────────────────────── + actions::NAV_DOWN if cur == Pane::List => { + let len = list.items.with_untracked(Vec::len); + if len > 0 { + list.cursor.update(|c| *c = (*c + count).min(len - 1)); + } + } + actions::NAV_UP if cur == Pane::List => { + list.cursor.update(|c| *c = c.saturating_sub(count)); + } + actions::NAV_TOP if cur == Pane::List => list.cursor.set(0), + actions::NAV_BOTTOM if cur == Pane::List => { + let len = list.items.with_untracked(Vec::len); + if len > 0 { + list.cursor.set(len - 1); + } + } + actions::NAV_PAGE_DOWN if cur == Pane::List => { + let len = list.items.with_untracked(Vec::len); + if len > 0 { + list.cursor.update(|c| *c = (*c + 10 * count).min(len - 1)); + } + } + actions::NAV_PAGE_UP if cur == Pane::List => { + list.cursor.update(|c| *c = c.saturating_sub(10 * count)); + } + + _ => {} + } +} + +// ── Tree helpers ────────────────────────────────────────────────────────────── + +fn tree_expand( + cursor: RwSignal, + rows: Memo>, + expanded: RwSignal>, +) { + let id = rows.with_untracked(|r| r.get(cursor.get_untracked()).map(|row| row.id.clone())); + if let Some(id) = id { + expanded.update(|e| { e.insert(id.clone()); }); + // Child loading is handled reactively by LibraryTree's Effect. + } +} + +fn tree_collapse( + cursor: RwSignal, + rows: Memo>, + expanded: RwSignal>, +) { + let id = rows.with_untracked(|r| r.get(cursor.get_untracked()).map(|row| row.id.clone())); + if let Some(id) = id { + expanded.update(|e| { e.remove(&id); }); + } +} + diff --git a/src/src/lib_tree.rs b/src/src/lib_tree.rs new file mode 100644 index 0000000..e3c0e14 --- /dev/null +++ b/src/src/lib_tree.rs @@ -0,0 +1,323 @@ +//! Library tree: flattening logic and the left-pane component. + +use std::collections::{HashMap, HashSet}; + +use leptos::prelude::*; +use leptos::task::spawn_local; +use web_sys::DragEvent; + +use crate::models::{Library, LibraryId, ReferenceId}; + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +/// Load `id`'s children from the backend and cache them, unless already cached. +pub fn load_children_if_needed(id: String, children_cache: RwSignal>>) { + let already = children_cache.with_untracked(|c| c.contains_key(&id)); + if !already { + spawn_local(async move { + let lib_id = LibraryId(id.clone()); + match crate::tauri::list_child_libraries(&lib_id).await { + Ok(children) => children_cache.update(|c| { c.insert(id, children); }), + Err(e) => leptos::logging::error!("load children ({id}): {e}"), + } + }); + } +} + +// ── Tree logic ──────────────────────────────────────────────────────────────── + +/// A single visible row in the rendered library tree. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TreeRow { + /// Library ID string (UUID). + pub id: String, + /// Display name. + pub name: String, + /// Nesting depth (0 = root). + pub depth: usize, + /// Whether this node is currently expanded. + pub expanded: bool, + /// `true` if children have not been loaded yet, or the node has children. + /// `false` only when children were loaded and the result was empty. + pub may_have_children: bool, +} + +/// Flatten the visible portion of the library hierarchy into an ordered list. +/// +/// Only nodes whose ancestors are all expanded appear in the output. +/// Children are ordered as returned by `children_cache`. +pub fn flatten_tree( + libs: &[Library], + children_cache: &HashMap>, + expanded: &HashSet, + depth: usize, +) -> Vec { + let mut rows = Vec::new(); + for lib in libs { + let id = lib.id.0.clone(); + let is_expanded = expanded.contains(&id); + let children = children_cache.get(&id); + let may_have_children = children.is_none_or(|c| !c.is_empty()); + + rows.push(TreeRow { + id: id.clone(), + name: lib.name.clone(), + depth, + expanded: is_expanded, + may_have_children, + }); + + if is_expanded { + if let Some(child_libs) = children { + rows.extend(flatten_tree(child_libs, children_cache, expanded, depth + 1)); + } + } + } + rows +} + +// ── Component ───────────────────────────────────────────────────────────────── + +/// Left pane: renders the library tree. +/// +/// Navigation (j/k, expand/collapse) is driven entirely from the parent via +/// `cursor` and `expanded`. Click on a row updates `cursor`. +/// +/// Each tree row is also a drag-drop target: dropping a publication item onto a +/// row calls `add_to_library`, adding the reference to that library. +#[component] +pub fn LibraryTree( + root_libs: RwSignal>, + children_cache: RwSignal>>, + expanded: RwSignal>, + cursor: RwSignal, + focused: RwSignal, +) -> impl IntoView { + use crate::lib_tab::Pane; + use leptos::either::Either; + + let rows = Memo::new(move |_| { + flatten_tree(&root_libs.get(), &children_cache.get(), &expanded.get(), 0) + }); + + // Reactively load children for any newly-expanded node. + // Using an Effect guarantees this runs in a proper reactive owner context, + // which makes `spawn_local` and signal updates flush correctly. + Effect::new(move |_| { + let exp = expanded.get(); // subscribe to expansion changes + for id in exp { + load_children_if_needed(id, children_cache); + } + }); + + // Which tree-row ID (if any) is the current drag-over target. + let drag_over_id: RwSignal> = RwSignal::new(None); + + view! { +
+ {move || { + let row_list = rows.get(); + if row_list.is_empty() { + Either::Left(view! { +
"Open a repository to see libraries"
+ }) + } else { + let cursor_pos = cursor.get(); + Either::Right(view! { +
    + {row_list.into_iter().enumerate().map(|(i, row)| { + let indent_px = row.depth * 16; + let icon = if row.may_have_children { + if row.expanded { "▾ " } else { "▸ " } + } else { + " " + }; + let is_cursor = i == cursor_pos; + let row_may_have_children = row.may_have_children; + + // Clone row.id for each closure that captures it. + let row_id_click = row.id.clone(); + let row_id_class = row.id.clone(); + let row_id_over = row.id.clone(); + let row_id_drop = row.id.clone(); + + view! { +
  • + {icon} + {row.name.clone()} +
  • + } + }).collect::>()} +
+ }) + } + }} +
+ } +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::LibraryId; + + fn lib(id: &str, name: &str, parent: Option<&str>) -> Library { + Library { + id: LibraryId(id.into()), + name: name.into(), + parent_id: parent.map(|p| LibraryId(p.into())), + } + } + + #[test] + fn empty_root_produces_empty_rows() { + let rows = flatten_tree(&[], &Default::default(), &Default::default(), 0); + assert!(rows.is_empty()); + } + + #[test] + fn single_root_unloaded_children() { + let root = lib("r", "Root", None); + let rows = flatten_tree(&[root], &Default::default(), &Default::default(), 0); + assert_eq!(rows.len(), 1); + assert_eq!(rows[0].id, "r"); + assert_eq!(rows[0].name, "Root"); + assert_eq!(rows[0].depth, 0); + assert!(!rows[0].expanded); + // Unknown children → may_have_children = true + assert!(rows[0].may_have_children); + } + + #[test] + fn collapsed_node_hides_children() { + let parent = lib("p", "Parent", None); + let child = lib("c", "Child", Some("p")); + let mut cache = HashMap::new(); + cache.insert("p".into(), vec![child]); + // Not in expanded set → children hidden + let rows = flatten_tree(&[parent], &cache, &Default::default(), 0); + assert_eq!(rows.len(), 1); + assert!(rows[0].may_have_children); + } + + #[test] + fn expanded_node_shows_children() { + let parent = lib("p", "Parent", None); + let child = lib("c", "Child", Some("p")); + let mut cache = HashMap::new(); + cache.insert("p".into(), vec![child]); + let mut expanded = HashSet::new(); + expanded.insert("p".into()); + + let rows = flatten_tree(&[parent], &cache, &expanded, 0); + assert_eq!(rows.len(), 2); + assert_eq!(rows[0].name, "Parent"); + assert!(rows[0].expanded); + assert_eq!(rows[1].name, "Child"); + assert_eq!(rows[1].depth, 1); + } + + #[test] + fn loaded_empty_children_marks_leaf() { + let node = lib("n", "Node", None); + let mut cache = HashMap::new(); + cache.insert("n".into(), vec![]); // explicitly empty + let rows = flatten_tree(&[node], &cache, &Default::default(), 0); + assert_eq!(rows.len(), 1); + assert!(!rows[0].may_have_children); + } + + #[test] + fn multi_level_nesting() { + let root = lib("r", "Root", None); + let mid = lib("m", "Mid", Some("r")); + let leaf = lib("l", "Leaf", Some("m")); + + let mut cache = HashMap::new(); + cache.insert("r".into(), vec![mid]); + cache.insert("m".into(), vec![leaf]); + + let mut expanded = HashSet::new(); + expanded.insert("r".into()); + expanded.insert("m".into()); + + let rows = flatten_tree(&[root], &cache, &expanded, 0); + assert_eq!(rows.len(), 3); + assert_eq!(rows[0].depth, 0); + assert_eq!(rows[1].depth, 1); + assert_eq!(rows[2].depth, 2); + assert_eq!(rows[2].name, "Leaf"); + } + + #[test] + fn multiple_roots_ordered() { + let a = lib("a", "Alpha", None); + let b = lib("b", "Beta", None); + let rows = flatten_tree(&[a, b], &Default::default(), &Default::default(), 0); + assert_eq!(rows.len(), 2); + assert_eq!(rows[0].name, "Alpha"); + assert_eq!(rows[1].name, "Beta"); + } + + #[test] + fn only_expanded_subtrees_included() { + // Parent expanded, but sibling collapsed + let p = lib("p", "P", None); + let s = lib("s", "S", None); // sibling root, also collapsed + let c = lib("c", "C", Some("p")); + let mut cache = HashMap::new(); + cache.insert("p".into(), vec![c]); + cache.insert("s".into(), vec![lib("sc", "SC", Some("s"))]); + + let mut expanded = HashSet::new(); + expanded.insert("p".into()); // expand only P + + let rows = flatten_tree(&[p, s], &cache, &expanded, 0); + assert_eq!(rows.len(), 3); // P, C, S (SC hidden) + assert_eq!(rows[0].name, "P"); + assert_eq!(rows[1].name, "C"); + assert_eq!(rows[2].name, "S"); + } +} diff --git a/src/src/main.rs b/src/src/main.rs new file mode 100644 index 0000000..8d0459c --- /dev/null +++ b/src/src/main.rs @@ -0,0 +1,419 @@ +mod command_bar; +mod commands; +mod lib_tab; +mod lib_tree; +mod models; +mod mode; +mod pdf_viewer; +mod pub_detail; +mod pub_list; +mod tab_bar; +mod tauri; + +use std::{ + cell::{Cell, RefCell}, + rc::Rc, +}; + +use brittle_keymap::{actions, Key, KeyCode, KeymapState, Outcome, default_bindings}; +use command_bar::{CommandBar, provide_search_query}; +use leptos::prelude::*; +use lib_tab::LibTab; +use mode::{AppMode, provide_mode}; +use pdf_viewer::PdfViewer; +use tab_bar::TabBar; +use web_sys::KeyboardEvent; + +fn main() { + leptos::mount::mount_to_body(App); +} + +// ── Action event ────────────────────────────────────────────────────────────── + +/// A dispatched keymap action. +/// +/// The `seq` field increments monotonically so that firing the same action +/// twice in succession produces a distinct signal value and triggers +/// reactive updates both times. +#[derive(Clone, PartialEq, Eq)] +pub struct ActionEvent { + pub name: String, + pub count: u32, + seq: u32, +} + +thread_local! { + static ACTION_SEQ: Cell = const { Cell::new(0) }; +} + +pub fn next_seq() -> u32 { + ACTION_SEQ.with(|s| { + let n = s.get(); + s.set(n.wrapping_add(1)); + n + }) +} + +/// Context handle giving components read access to the last dispatched action. +#[derive(Clone, Copy)] +pub struct KeymapAction(pub ReadSignal>); + +// ── Tab types ───────────────────────────────────────────────────────────────── + +/// A single open tab in the application. +#[derive(Clone, PartialEq, Eq)] +pub enum AppTab { + /// The persistent library / reference browser tab (always index 0). + Library, + /// A PDF viewer tab for a specific reference. + Pdf { + /// UUID string of the reference whose PDF is being viewed. + ref_id: String, + /// Short display name shown in the tab (typically the cite key). + title: String, + }, +} + +// ── Theme context ───────────────────────────────────────────────────────────── + +/// Context handle for the current colour theme (`"dark"` or `"light"`). +/// +/// Writable so `CommandBar` can update it when the user types `:theme …`. +#[derive(Clone, Copy)] +pub struct ThemeContext(pub RwSignal); + +fn provide_theme() { + let theme = RwSignal::new("dark".to_string()); + provide_context(ThemeContext(theme)); + + // Reactively apply the `data-theme` attribute to ``. + Effect::new(move |_| { + let t = theme.get(); + if let Some(doc) = web_sys::window().and_then(|w| w.document()) { + if let Some(el) = doc.document_element() { + let _ = el.set_attribute("data-theme", &t); + } + } + }); + + // Load the persisted theme from backend config; replace the default if + // found. Runs after the effect is live, so the DOM is updated immediately + // once the IPC call returns. + leptos::task::spawn_local(async move { + if let Ok(t) = crate::tauri::get_theme().await { + theme.set(t); + } + }); +} + +// ── Reload trigger ──────────────────────────────────────────────────────────── + +/// Incrementing counter that components watch to know when to reload their data. +/// +/// Incremented whenever the open repository changes (`:open `). +#[derive(Clone, Copy)] +pub struct ReloadTrigger(pub RwSignal); + +// ── PDF open context ────────────────────────────────────────────────────────── + +/// Request to open (or switch to) a PDF tab, posted by child components. +#[derive(Clone, PartialEq, Eq)] +pub struct PdfOpenRequest { + pub ref_id: String, + pub title: String, +} + +/// Context handle that child components write to when they want to open a PDF tab. +#[derive(Clone, Copy)] +pub struct OpenPdfContext(pub RwSignal>); + +// ── Keymap provider ──────────────────────────────────────────────────────────── + +fn provide_keymap() { + let state: Rc> = + Rc::new(RefCell::new(KeymapState::new(default_bindings()))); + + let (action_read, action_write) = signal::>(None); + provide_context(KeymapAction(action_read)); + + let state_for_listener = state.clone(); + let listener = window_event_listener(leptos::ev::keydown, move |ev: KeyboardEvent| { + if is_input_focused() { + return; + } + if let Some(key) = key_from_event(&ev) { + ev.prevent_default(); + let outcome = state_for_listener.borrow_mut().process(key); + if let Outcome::Action { name, count } = outcome { + action_write.set(Some(ActionEvent { name, count, seq: next_seq() })); + } + } + }); + + on_cleanup(move || listener.remove()); + + // Asynchronously load user keybinding overrides and hot-swap the state. + // Runs after the listener is already live, so the app is usable with + // defaults while the async IPC call is in flight. + let state_for_overrides = state.clone(); + leptos::task::spawn_local(async move { + let overrides = match crate::tauri::get_keybindings().await { + Ok(map) if !map.is_empty() => map, + _ => return, // no config or error — keep defaults + }; + + let mut bindings = default_bindings(); + // Config keys use snake_case; action names use dot.notation. + let pairs: Vec<(String, String)> = overrides + .into_iter() + .map(|(k, v)| (action_key_to_name(&k), v)) + .collect(); + bindings.apply_overrides(pairs.iter().map(|(k, v)| (k.as_str(), v.as_str()))); + *state_for_overrides.borrow_mut() = KeymapState::new(bindings); + }); +} + +/// Convert a config keybinding key (snake_case) to an action name (dot.notation). +/// +/// ``` +/// # use brittle_ui::action_key_to_name; // won't compile as-is, just illustrating +/// assert_eq!(action_key_to_name("tab_next"), "tab.next"); +/// assert_eq!(action_key_to_name("nav_page_down"), "nav.page.down"); +/// ``` +fn action_key_to_name(key: &str) -> String { + key.replace('_', ".") +} + +// ── Helpers ──────────────────────────────────────────────────────────────────── + +fn is_input_focused() -> bool { + let Some(doc) = web_sys::window().and_then(|w| w.document()) else { + return false; + }; + let Some(el) = doc.active_element() else { + return false; + }; + let tag = el.tag_name().to_uppercase(); + if tag == "INPUT" || tag == "TEXTAREA" { + return true; + } + el.get_attribute("contenteditable") + .map(|v| v != "false") + .unwrap_or(false) +} + +fn key_from_event(ev: &KeyboardEvent) -> Option { + let key_str = ev.key(); + let code = match key_str.as_str() { + "Enter" => KeyCode::Enter, + "Escape" => KeyCode::Escape, + "Tab" => KeyCode::Tab, + "Backspace" => KeyCode::Backspace, + "Delete" => KeyCode::Delete, + " " => KeyCode::Space, + "ArrowUp" => KeyCode::ArrowUp, + "ArrowDown" => KeyCode::ArrowDown, + "ArrowLeft" => KeyCode::ArrowLeft, + "ArrowRight" => KeyCode::ArrowRight, + "Home" => KeyCode::Home, + "End" => KeyCode::End, + "PageUp" => KeyCode::PageUp, + "PageDown" => KeyCode::PageDown, + s if s.starts_with('F') && s.len() > 1 => KeyCode::F(s[1..].parse::().ok()?), + s if s.chars().count() == 1 => KeyCode::Char(s.chars().next().unwrap()), + _ => return None, + }; + + // For Char keys the character already encodes the shift state: + // ':' is the shifted ';', 'G' is the shifted 'g', etc. + // Including shift: true would produce which never matches a binding. + // For special keys (Tab, arrows, …) shift must be preserved (). + let shift = match code { + KeyCode::Char(_) => false, + _ => ev.shift_key(), + }; + + Some(Key { + code, + ctrl: ev.ctrl_key(), + shift, + alt: ev.alt_key(), + meta: ev.meta_key(), + }) +} + +// ── Root component ───────────────────────────────────────────────────────────── + +#[component] +fn App() -> impl IntoView { + provide_keymap(); + provide_theme(); + let mode = provide_mode(); + provide_search_query(); + + // ── Tab state ───────────────────────────────────────────────────────────── + // The Library tab is always present at index 0 and cannot be closed. + let tabs = RwSignal::new(vec![AppTab::Library]); + let active_tab = RwSignal::new(0usize); + + // Provide the open-PDF context so LibTab can post open requests. + let open_pdf_req = RwSignal::new(Option::::None); + provide_context(OpenPdfContext(open_pdf_req)); + + // Reload trigger: increment when a repository is opened. + provide_context(ReloadTrigger(RwSignal::new(0u32))); + + // ── Keymap wiring ───────────────────────────────────────────────────────── + let keymap_action = use_context::().unwrap().0; + Effect::new(move |_| { + let Some(ev) = keymap_action.get() else { return }; + match ev.name.as_str() { + // Mode switching + actions::MODE_COMMAND => mode.set(AppMode::Command), + actions::MODE_SEARCH => mode.set(AppMode::Search), + actions::MODE_NORMAL => mode.set(AppMode::Normal), + + // Tab cycling + actions::TAB_NEXT => { + let len = tabs.with_untracked(Vec::len); + if len > 1 { + active_tab.update(|i| *i = (*i + 1) % len); + } + } + actions::TAB_PREV => { + let len = tabs.with_untracked(Vec::len); + if len > 1 { + active_tab.update(|i| *i = if *i == 0 { len - 1 } else { *i - 1 }); + } + } + + // Close the active tab (Library tab is immortal) + actions::TAB_CLOSE => { + let idx = active_tab.get_untracked(); + if idx > 0 { + tabs.update(|t| { t.remove(idx); }); + active_tab.update(|i| *i = i.saturating_sub(1)); + } + } + + _ => {} + } + }); + + // ── Open-PDF request handler ─────────────────────────────────────────────── + // Watches for requests from LibTab and opens or activates the PDF tab. + Effect::new(move |_| { + let Some(req) = open_pdf_req.get() else { return }; + + let existing = tabs.with_untracked(|t| { + t.iter().position(|tab| { + matches!(tab, AppTab::Pdf { ref_id: r, .. } if *r == req.ref_id) + }) + }); + + if let Some(idx) = existing { + active_tab.set(idx); + } else { + let new_idx = tabs.with_untracked(Vec::len); + tabs.update(|t| t.push(AppTab::Pdf { + ref_id: req.ref_id.clone(), + title: req.title.clone(), + })); + active_tab.set(new_idx); + } + + // Clear so the same ref_id can re-trigger (e.g. switch away, then re-open). + open_pdf_req.set(None); + }); + + // ── View ────────────────────────────────────────────────────────────────── + view! { +
+ +
+ // Library tab — always mounted; hidden when a PDF tab is active. +
+ +
+ // PDF tabs — each mounted once (keyed by ref_id) and hidden + // when not active, so iframe state is preserved across switches. + Some((i, ref_id)), + AppTab::Library => None, + }) + .collect::>() + } + key=|(_, ref_id)| ref_id.clone() + children=move |(i, ref_id)| { + // Derive visibility reactively: look up the live tabs + // so the style updates correctly after tab close/reorder. + let ref_id_vis = ref_id.clone(); + let is_visible = move || { + let active = active_tab.get(); + tabs.with(|t| { + t.get(active) + .map(|tab| matches!( + tab, + AppTab::Pdf { ref_id: r, .. } if *r == ref_id_vis + )) + .unwrap_or(false) + }) + }; + // `i` at creation time is used as a fallback for the + // close button in TabBar; here we only need visibility. + let _ = i; + view! { +
+ +
+ } + } + /> +
+ +
+ } +} + +// ── Tests ────────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::action_key_to_name; + + #[test] + fn single_segment_unchanged() { + assert_eq!(action_key_to_name("quit"), "quit"); + } + + #[test] + fn two_segment_conversion() { + assert_eq!(action_key_to_name("tab_next"), "tab.next"); + assert_eq!(action_key_to_name("tab_prev"), "tab.prev"); + assert_eq!(action_key_to_name("tab_close"), "tab.close"); + } + + #[test] + fn three_segment_conversion() { + assert_eq!(action_key_to_name("nav_page_down"), "nav.page.down"); + assert_eq!(action_key_to_name("nav_page_up"), "nav.page.up"); + } + + #[test] + fn focus_actions() { + assert_eq!(action_key_to_name("focus_left"), "focus.left"); + assert_eq!(action_key_to_name("focus_center"), "focus.center"); + assert_eq!(action_key_to_name("focus_right"), "focus.right"); + } + + #[test] + fn mode_actions() { + assert_eq!(action_key_to_name("mode_command"), "mode.command"); + assert_eq!(action_key_to_name("mode_normal"), "mode.normal"); + } +} diff --git a/src/src/mode.rs b/src/src/mode.rs new file mode 100644 index 0000000..bb2e27a --- /dev/null +++ b/src/src/mode.rs @@ -0,0 +1,24 @@ +//! Application mode state. +//! +//! Brittle has three modes: +//! - **Normal** — keyboard shortcuts are active; the default state. +//! - **Command** — the `:` prompt is open; user types a command. +//! - **Search** — the `/` prompt is open; user types a search/filter query. + +use leptos::prelude::*; + +/// The current input mode of the application. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum AppMode { + Normal, + Command, + Search, +} + +/// Create the application mode signal. +/// +/// Returns the [`RwSignal`] which should be passed to components that need it. +/// Call once from the root component. +pub fn provide_mode() -> RwSignal { + RwSignal::new(AppMode::Normal) +} diff --git a/src/src/models.rs b/src/src/models.rs new file mode 100644 index 0000000..0a0f6b5 --- /dev/null +++ b/src/src/models.rs @@ -0,0 +1,256 @@ +//! Frontend mirror of `brittle-core` data types. +//! +//! These types are intentionally minimal — they carry only the fields the +//! frontend needs. Unknown fields coming from the Tauri IPC are silently +//! ignored by serde's default behaviour. + +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +// ── IDs ─────────────────────────────────────────────────────────────────────── + +/// A library identifier (UUID string). +#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] +#[serde(transparent)] +pub struct LibraryId(pub String); + +/// A reference identifier (UUID string). +#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] +#[serde(transparent)] +pub struct ReferenceId(pub String); + +// ── Supporting types ────────────────────────────────────────────────────────── + +/// A person (author, editor, etc.). +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +pub struct Person { + pub family: String, + pub given: Option, + pub prefix: Option, + pub suffix: Option, +} + +impl Person { + /// "Given Family" or just "Family" when given is absent. + pub fn display_name(&self) -> String { + match &self.given { + Some(g) => format!("{} {}", g, self.family), + None => self.family.clone(), + } + } +} + +/// BibTeX entry type. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum EntryType { + Article, + Book, + Booklet, + InBook, + InCollection, + InProceedings, + Manual, + MastersThesis, + Misc, + PhdThesis, + Proceedings, + TechReport, + Unpublished, + Online, +} + +impl std::fmt::Display for EntryType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + EntryType::Article => "article", + EntryType::Book => "book", + EntryType::Booklet => "booklet", + EntryType::InBook => "inbook", + EntryType::InCollection => "incollection", + EntryType::InProceedings => "inproceedings", + EntryType::Manual => "manual", + EntryType::MastersThesis => "mastersthesis", + EntryType::Misc => "misc", + EntryType::PhdThesis => "phdthesis", + EntryType::Proceedings => "proceedings", + EntryType::TechReport => "techreport", + EntryType::Unpublished => "unpublished", + EntryType::Online => "online", + }; + write!(f, "{s}") + } +} + +// ── Library ─────────────────────────────────────────────────────────────────── + +/// A library node in the tree (subset of `brittle_core::Library`). +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +pub struct Library { + pub id: LibraryId, + pub name: String, + pub parent_id: Option, +} + +// ── References ──────────────────────────────────────────────────────────────── + +/// A lightweight reference record used in the publication list. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +pub struct ReferenceSummary { + pub id: ReferenceId, + pub cite_key: String, + pub entry_type: EntryType, + pub title: Option, + pub authors: Vec, + pub year: Option, +} + +impl ReferenceSummary { + /// Title or "[no title]" fallback. + pub fn title_display(&self) -> &str { + self.title.as_deref().unwrap_or("[no title]") + } + + /// Compact author string: "Family", "A & B", or "A et al." + pub fn author_display(&self) -> String { + match self.authors.len() { + 0 => "—".into(), + 1 => self.authors[0].family.clone(), + 2 => format!("{} & {}", self.authors[0].family, self.authors[1].family), + _ => format!("{} et al.", self.authors[0].family), + } + } +} + +/// Full reference record used in the detail pane. +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct Reference { + pub id: ReferenceId, + pub cite_key: String, + pub entry_type: EntryType, + pub authors: Vec, + pub editors: Vec, + /// BibTeX fields: title, year, journal, doi, abstract, … + pub fields: BTreeMap, + pub created_at: String, + pub modified_at: String, +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::from_str; + + #[test] + fn library_id_transparent() { + let id: LibraryId = from_str(r#""abc-123""#).unwrap(); + assert_eq!(id.0, "abc-123"); + + // Also verify round-trips back to a plain string. + let back = serde_json::to_string(&id).unwrap(); + assert_eq!(back, r#""abc-123""#); + } + + #[test] + fn library_ignores_extra_fields() { + // brittle-core sends members, timestamps, etc. — we ignore them. + let json = r#"{ + "id": "lib-1", + "name": "Physics", + "parent_id": null, + "members": ["ref-x"], + "created_at": "2024-01-01T00:00:00Z", + "modified_at": "2024-01-01T00:00:00Z" + }"#; + let lib: Library = from_str(json).unwrap(); + assert_eq!(lib.id.0, "lib-1"); + assert_eq!(lib.name, "Physics"); + assert_eq!(lib.parent_id, None); + } + + #[test] + fn library_with_parent() { + let json = r#"{"id": "child-1", "name": "QM", "parent_id": "lib-1"}"#; + let lib: Library = from_str(json).unwrap(); + assert_eq!(lib.parent_id, Some(LibraryId("lib-1".into()))); + } + + #[test] + fn reference_summary_fields() { + let json = r#"{ + "id": "ref-1", + "cite_key": "einstein1905", + "entry_type": "article", + "title": "On the Electrodynamics of Moving Bodies", + "authors": [{"family": "Einstein", "given": "Albert", "prefix": null, "suffix": null}], + "year": "1905" + }"#; + let rs: ReferenceSummary = from_str(json).unwrap(); + assert_eq!(rs.cite_key, "einstein1905"); + assert_eq!(rs.title_display(), "On the Electrodynamics of Moving Bodies"); + assert_eq!(rs.author_display(), "Einstein"); + assert_eq!(rs.year.as_deref(), Some("1905")); + } + + #[test] + fn reference_summary_no_title() { + let rs = ReferenceSummary { + id: ReferenceId("x".into()), + cite_key: "x".into(), + entry_type: EntryType::Misc, + title: None, + authors: vec![], + year: None, + }; + assert_eq!(rs.title_display(), "[no title]"); + assert_eq!(rs.author_display(), "—"); + } + + #[test] + fn author_display_variants() { + fn p(family: &str) -> Person { + Person { family: family.into(), given: None, prefix: None, suffix: None } + } + let base = ReferenceSummary { + id: ReferenceId("x".into()), + cite_key: "x".into(), + entry_type: EntryType::Article, + title: None, + authors: vec![], + year: None, + }; + let one = ReferenceSummary { authors: vec![p("Smith")], ..base.clone() }; + assert_eq!(one.author_display(), "Smith"); + + let two = ReferenceSummary { authors: vec![p("Smith"), p("Jones")], ..base.clone() }; + assert_eq!(two.author_display(), "Smith & Jones"); + + let many = ReferenceSummary { + authors: vec![p("Smith"), p("Jones"), p("Brown")], + ..base + }; + assert_eq!(many.author_display(), "Smith et al."); + } + + #[test] + fn person_display_name() { + let p = Person { + family: "Turing".into(), + given: Some("Alan".into()), + prefix: None, + suffix: None, + }; + assert_eq!(p.display_name(), "Alan Turing"); + + let p2 = Person { given: None, ..p }; + assert_eq!(p2.display_name(), "Turing"); + } + + #[test] + fn entry_type_display() { + assert_eq!(EntryType::Article.to_string(), "article"); + assert_eq!(EntryType::InProceedings.to_string(), "inproceedings"); + } +} diff --git a/src/src/pdf_viewer.rs b/src/src/pdf_viewer.rs new file mode 100644 index 0000000..647c46b --- /dev/null +++ b/src/src/pdf_viewer.rs @@ -0,0 +1,28 @@ +//! PDF viewer tab: embeds the Tauri-served PDF viewer in an iframe. +//! +//! The custom `brittle://` URI scheme serves: +//! - `brittle://app/viewer?ref_id=` — the viewer HTML page (PDF.js) +//! - `brittle://app/pdf?ref_id=` — the raw PDF bytes +//! +//! Using an `